Apache CXF-Unterstützung für RESTful-Webdienste

Apache CXF-Unterstützung für RESTful Web Services

1. Überblick

In diesem Lernprogramm wirdApache CXF als Framework vorgestellt, das dem JAX-RS-Standard entspricht, der die Unterstützung des Java-Ökosystems für das Architekturmuster REpresentational State Transfer (REST) ​​definiert.

Insbesondere wird Schritt für Schritt beschrieben, wie ein RESTful-Webdienst erstellt und veröffentlicht wird und wie Komponententests zur Überprüfung eines Dienstes geschrieben werden.

Dies ist der dritte Teil einer Reihe zu Apache CXF. the first one konzentriert sich auf die Verwendung von CXF als JAX-WS-kompatible Implementierung. Diesecond article bieten eine Anleitung zur Verwendung von CXF mit Spring.

2. Maven-Abhängigkeiten

Die erste erforderliche Abhängigkeit istorg.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

In diesem Lernprogramm verwenden wir CXF, um einenServer-Endpunkt zum Veröffentlichen eines Webdienstes zu erstellen, anstatt einen Servlet-Container zu verwenden. Daher muss die folgende Abhängigkeit in die Maven-POM-Datei aufgenommen werden:


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

Fügen wir abschließend die HttpClient-Bibliothek hinzu, um Unit-Tests zu vereinfachen:


    org.apache.httpcomponents
    httpclient
    4.5.2

Here finden Sie die neueste Version dercxf-rt-frontend-jaxrs-Abhängigkeit. Möglicherweise möchten Sie auch aufthis link verweisen, um die neuesten Versionen der Artefakte vonorg.apache.cxf:cxf-rt-transports-http-jettyzu erhalten. Schließlich kann die neueste Version vonhttpclienthere gefunden werden.

3. Ressourcenklassen und Anforderungszuordnung

Beginnen wir mit der Implementierung eines einfachen Beispiels. Wir werden unsere REST-API mit zwei RessourcenCourse undStudent. einrichten

Wir werden einfach anfangen und uns im Laufe der Zeit einem komplexeren Beispiel zuwenden.

3.1. Die Ressourcen

Hier ist die Definition der RessourcenklasseStudent:

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

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

}

Beachten Sie, dass wir die Annotation@XmlRootElementverwenden, um JAXB mitzuteilen, dass Instanzen dieser Klasse in XML gemarshallt werden sollen.

Als nächstes folgt die Definition der RessourcenklasseCourse:

@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

}

Lassen Sie uns abschließendCourseRepository implementieren - dies ist die Stammressource und dient als Einstiegspunkt für Webdienstressourcen:

@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;
    }
}

Beachten Sie die Zuordnung mit der Annotation@Path. CourseRepository ist hier die Stammressource, daher ist es so zugeordnet, dass alle URLs verarbeitet werden, die mitcoursebeginnen.

Der Wert der Annotation@Produceswird verwendet, um den Server anzuweisen, von Methoden innerhalb dieser Klasse zurückgegebene Objekte in XML-Dokumente zu konvertieren, bevor sie an Clients gesendet werden. Wir verwenden hier standardmäßig JAXB, da keine anderen Bindungsmechanismen angegeben sind.

3.2. Einfache Dateneinrichtung

Da dies eine einfache Beispielimplementierung ist, verwenden wir In-Memory-Daten anstelle einer vollwertigen persistenten Lösung.

In diesem Sinne implementieren wir eine einfache Einrichtungslogik, um einige Daten in das System einzufügen:

{
    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);
}

Methoden innerhalb dieser Klasse, die sich um HTTP-Anforderungen kümmern, werden im nächsten Unterabschnitt behandelt.

3.3. Die API - Request Mapping-Methoden

Fahren wir nun mit der Implementierung der eigentlichen REST-API fort.

Wir werden mit dem Hinzufügen von API-Operationen beginnen - unter Verwendung der Annotation@Path- direkt in den Ressourcen-POJOs.

Es ist wichtig zu verstehen, dass dies ein wesentlicher Unterschied zum Ansatz in einem typischen Spring-Projekt ist, bei dem die API-Operationen in einem Controller und nicht auf dem POJO selbst definiert werden.

Beginnen wir mit Zuordnungsmethoden, die in der KlasseCoursedefiniert sind:

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

Einfach ausgedrückt wird die Methode aufgerufen, wennGET-Anforderungen verarbeitet werden, die durch die Annotation@GETgekennzeichnet sind.

Beachten Sie die einfache Syntax zum Zuordnen des PfadparametersstudentId aus der HTTP-Anforderung.

Wir verwenden dann einfach die HilfsmethodefindById, um die entsprechende Instanz vonStudentzurückzugeben.

Die folgende Methode verarbeitet die Anforderungen vonPOST, die durch die Annotation von@POSTangezeigt werden, indem das empfangene Objekt vonStudentzur Liste vonstudentshinzugefügt wird:

@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();
}

Dies gibt eine200 OK-Antwort zurück, wenn die Erstellungsoperation erfolgreich war, oder409 Conflict, wenn ein Objekt mit den übermitteltenid bereits vorhanden ist.

Beachten Sie auch, dass wir die Annotation von@Pathüberspringen können, da der Wert eine leere Zeichenfolge ist.

Die letzte Methode kümmert sich umDELETE Anfragen. Es entfernt ein Element aus der Listestudents, dessenid der empfangene Pfadparameter ist, und gibt eine Antwort mit dem StatusOK (200) zurück. Falls den angegebenenid keine Elemente zugeordnet sind, was bedeutet, dass nichts entfernt werden muss, gibt diese Methode eine Antwort mit dem StatusNot Found (404) zurück:

@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();
}

Fahren wir fort, um Zuordnungsmethoden der KlasseCourseRepositoryanzufordern.

Die folgendegetCourse-Methode gibt einCourse-Objekt zurück, das der Wert eines Eintrags in dercourses-Map ist, dessen Schlüssel der empfangenecourseId-Pfadparameter vonGET ist Anfrage. Intern sendet die Methode Pfadparameter an die HilfsmethodefindById, um ihre Aufgabe zu erledigen.

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

Die folgende Methode aktualisiert einen vorhandenen Eintrag in der Zuordnung voncourses, wobei der Hauptteil der empfangenen Anforderung vonPUTder Eingabewert und der ParametercourseIdder zugehörige Schlüssel ist:

@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();
}

Diese MethodeupdateCourse gibt eine Antwort mit dem StatusOK (200) zurück, wenn die Aktualisierung erfolgreich ist, ändert nichts und gibt eine AntwortNot Modified (304) zurück, wenn die vorhandenen und hochgeladenen Objekte die haben gleiche Feldwerte. Falls eineCourse-Instanz mit dem angegebenenid nicht in dercourses-Zuordnung gefunden wird, gibt die Methode eine Antwort mit dem StatusNot Found (404) zurück.

Die dritte Methode dieser Stammressourcenklasse verarbeitet keine HTTP-Anforderung direkt. Stattdessen werden Anforderungen an die KlasseCoursedelegiert, in der Anforderungen mit übereinstimmenden Methoden verarbeitet werden:

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

Wir haben Methoden innerhalb derCourse-Klasse gezeigt, die delegierte Anforderungen unmittelbar zuvor verarbeiten.

4. Server Endpunkt

Dieser Abschnitt konzentriert sich auf den Aufbau eines CXF-Servers, der zum Veröffentlichen des RESTful-Webdienstes verwendet wird, dessen Ressourcen im vorherigen Abschnitt dargestellt sind. Der erste Schritt besteht darin, das ObjektJAXRSServerFactoryBeanzu instanziieren und die Stammressourcenklasse festzulegen:

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

In der Factory-Bean muss dann ein Ressourcenanbieter festgelegt werden, um den Lebenszyklus der Stammressourcenklasse zu verwalten. Wir verwenden den Standard-Singleton-Ressourcenanbieter, der für jede Anforderung dieselbe Ressourceninstanz zurückgibt:

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

Wir legen auch eine Adresse fest, um die URL anzugeben, unter der der Webdienst veröffentlicht wird:

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

Jetzt können mitfactoryBean neueserver erstellt werden, die auf eingehende Verbindungen warten:

Server server = factoryBean.create();

Der gesamte Code in diesem Abschnitt sollte in die Methodemain eingeschlossen werden:

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

Der Aufruf diesermain-Methode ist in Abschnitt 6 dargestellt.

5. Testfälle

In diesem Abschnitt werden Testfälle beschrieben, mit denen der zuvor erstellte Webdienst überprüft wird. Diese Tests validieren die Ressourcenzustände des Dienstes, nachdem sie auf HTTP-Anforderungen der vier am häufigsten verwendeten Methoden geantwortet haben, nämlichGET,POST,PUT undDELETE.

5.1. Vorbereitung

Zunächst werden innerhalb der Testklasse zwei statische Felder mit dem NamenRestfulTest deklariert:

private static String BASE_URL = "http://localhost:8080/example/courses/";
private static CloseableHttpClient client;

Bevor wir Tests ausführen, erstellen wir einclient-Objekt, das zur Kommunikation mit dem Server verwendet und anschließend zerstört wird:

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

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

Die Instanz vonclientkann jetzt von Testfällen verwendet werden.

5.2. GET Anfragen

In der Testklasse definieren wir zwei Methoden, umGET-Anforderungen an den Server zu senden, auf dem der Webdienst ausgeführt wird.

Die erste Methode besteht darin, eineCourse-Instanz mitid in der Ressource abzurufen:

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

Die zweite besteht darin, eineStudent-Instanz zu erhalten, wenn dieids des Kurses und des Schülers in der Ressource angegeben sind:

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

Diese Methoden senden die Anforderungen von HTTPGETan die Serviceressource und deaktivieren dann die XML-Antworten für Instanzen der entsprechenden Klassen. Beide werden verwendet, um den Status der Serviceressourcen nach Ausführung der AnforderungenPOST,PUT undDELETE zu überprüfen.

5.3. POST Anfragen

Dieser Unterabschnitt enthält zwei Testfälle fürPOST-Anforderungen, die den Betrieb des Webdienstes veranschaulichen, wenn die hochgeladeneStudent-Instanz zu einem Konflikt führt und wenn sie erfolgreich erstellt wurde.

Im ersten Test verwenden wir einStudent-Objekt, das aus derconflict_student.xml-Datei im Klassenpfad mit folgendem Inhalt nicht gemarshallt wurde:


    2
    Student B

Auf diese Weise wird dieser Inhalt in den Anforderungshauptteil vonPOSTkonvertiert:

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

Der HeaderContent-Type wird festgelegt, um dem Server mitzuteilen, dass der Inhaltstyp der Anforderung XML ist:

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

Da das hochgeladeneStudent-Objekt bereits in der erstenCourse-Instanz vorhanden ist, erwarten wir, dass die Erstellung fehlschlägt und eine Antwort mit dem StatusConflict (409) zurückgegeben wird. Das folgende Code-Snippet überprüft die Erwartung:

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

Im nächsten Test extrahieren wir den Hauptteil einer HTTP-Anforderung aus einer Datei mit dem Namencreated_student.xml, ebenfalls im Klassenpfad. Hier ist der Inhalt der Datei:


    3
    Student C

Ähnlich wie im vorherigen Testfall wird eine Anforderung erstellt und ausgeführt. Anschließend wird überprüft, ob eine neue Instanz erfolgreich erstellt wurde:

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());

Wir können neue Zustände der Webdienst-Ressource bestätigen:

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

So sieht die XML-Antwort auf eine Anforderung für das neueStudent-Objekt aus:



    3
    Student C

5.4. PUT Anfragen

Beginnen wir mit einer ungültigen Aktualisierungsanforderung, bei der das zu aktualisierendeCourse-Objekt nicht vorhanden ist. Hier ist der Inhalt der Instanz, die zum Ersetzen eines nicht vorhandenenCourse-Objekts in der Webdienstressource verwendet wird:


    3
    Apache CXF Support for RESTful

Dieser Inhalt wird in einer Datei namensnon_existent_course.xml im Klassenpfad gespeichert. Es wird extrahiert und dann verwendet, um den Hauptteil einerPUT-Anforderung mit dem folgenden Code zu füllen:

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

Der HeaderContent-Type wird festgelegt, um dem Server mitzuteilen, dass der Inhaltstyp der Anforderung XML ist:

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

Da wir absichtlich eine ungültige Anforderung zum Aktualisieren eines nicht vorhandenen Objekts gesendet haben, wird erwartet, dass eineNot Found (404) -Antwort empfangen wird. Die Antwort ist validiert:

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

Im zweiten Testfall fürPUT-Anfragen senden wir einCourse-Objekt mit denselben Feldwerten. Da in diesem Fall nichts geändert wird, erwarten wir, dass eine Antwort mit dem StatusNot Modified (304) zurückgegeben wird. Der gesamte Prozess ist dargestellt:

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());

Wobeiunchanged_course.xml die Datei im Klassenpfad ist, in der die zum Aktualisieren verwendeten Informationen gespeichert sind. Hier ist sein Inhalt:


    1
    REST with Spring

In der letzten Demonstration der Anforderungen vonPUTführen wir eine gültige Aktualisierung durch. Das Folgende ist der Inhalt derchanged_course.xml-Datei, deren Inhalt zum Aktualisieren einerCourse-Instanz in der Webdienstressource verwendet wird:


    2
    Apache CXF Support for RESTful

So wird die Anfrage aufgebaut und ausgeführt:

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");

Überprüfen wir die Anforderung vonPUTan den Server und einen erfolgreichen Upload:

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

Überprüfen Sie den neuen Status der Webdienstressource:

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

Das folgende Codefragment zeigt den Inhalt der XML-Antwort, wenn eine GET-Anforderung für das zuvor hochgeladeneCourse-Objekt gesendet wird:



    2
    Apache CXF Support for RESTful

5.5. DELETE Anfragen

Versuchen wir zunächst, eine nicht vorhandeneStudent-Instanz zu löschen. Die Operation sollte fehlschlagen und eine entsprechende Antwort mit dem StatusNot Found (404) wird erwartet:

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

Im zweiten Testfall fürDELETE-Anfragen erstellen, führen und verifizieren wir eine Anfrage:

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

Wir überprüfen neue Zustände der Webservice-Ressource mit dem folgenden Code-Snippet:

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

Als Nächstes listen wir die XML-Antwort auf, die nach einer Anforderung für das ersteCourse-Objekt in der Webdienstressource empfangen wird:



    1
    REST with Spring
    
        2
        Student B
    

Es ist klar, dass die erstenStudent erfolgreich entfernt wurden.

6. Test Ausführung

In Abschnitt 4 wurde beschrieben, wie eineServer-Instanz in dermain-Methode derRestfulServer-Klasse erstellt und zerstört wird.

Der letzte Schritt, um den Server zum Laufen zu bringen, besteht darin, die Methodemainaufzurufen. Um dies zu erreichen, wird das Exec Maven-Plugin in die Maven-POM-Datei aufgenommen und konfiguriert:


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

Die neueste Version dieses Plugins finden Sie überthis link.

Beim Kompilieren und Packen des in diesem Tutorial dargestellten Artefakts führt das Maven Surefire-Plugin automatisch alle Tests aus, die in Klassen eingeschlossen sind, deren Namen mitTest beginnen oder enden. In diesem Fall sollte das Plugin so konfiguriert sein, dass diese Tests ausgeschlossen werden:


    maven-surefire-plugin
    2.19.1
    
    
        **/ServiceTest
    
    

Bei der obigen Konfiguration wirdServiceTest ausgeschlossen, da dies der Name der Testklasse ist. Sie können einen beliebigen Namen für diese Klasse auswählen, vorausgesetzt, die darin enthaltenen Tests werden vom Maven Surefire-Plugin nicht ausgeführt, bevor der Server für Verbindungen bereit ist.

Die neueste Version des Maven Surefire-Plugins finden Sie unterhere.

Jetzt können Sie das Ziel vonexec:javaausführen, um den RESTful-Webdienstserver zu starten, und dann die obigen Tests mit einer IDE ausführen. Entsprechend können Sie den Test starten, indem Sie den Befehlmvn -Dtest=ServiceTest test in a terminal. ausführen

7. Fazit

In diesem Tutorial wurde die Verwendung von Apache CXF als JAX-RS-Implementierung veranschaulicht. Es wurde gezeigt, wie das Framework zum Definieren von Ressourcen für einen RESTful-Webdienst und zum Erstellen eines Servers zum Veröffentlichen des Diensts verwendet werden kann.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inthe GitHub project.