Apache CXF-ondersteuning voor RESTful Web Services

1. Overzicht

Deze tutorial introduceert Apache CXF als een raamwerk dat voldoet aan de JAX-RS-standaard, die de ondersteuning van het Java-ecosysteem definieert voor het architectuurpatroon REpresentational State Transfer (REST).

In het bijzonder wordt stap voor stap beschreven hoe u een RESTful-webservice opbouwt en publiceert en hoe u unit-tests schrijft om een ​​service te verifiëren.

Dit is de derde in een serie over Apache CXF; de eerste richt zich op het gebruik van CXF als een volledig compatibele JAX-WS-implementatie. Het tweede artikel bevat een handleiding over het gebruik van CXF met Spring.

2. Maven afhankelijkheden

De eerste vereiste afhankelijkheid is org.apache.cxf: cxf- rt -frontend-jaxrs. Dit artefact biedt zowel JAX-RS API's als een CXF-implementatie:

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

In deze tutorial gebruiken we CXF om een Server endpoint om een ​​webservice te publiceren in plaats van een servletcontainer te gebruiken. Daarom moet de volgende afhankelijkheid worden opgenomen in het Maven POM-bestand:

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

Laten we tot slot de HttpClient-bibliotheek toevoegen om unit tests te vergemakkelijken:

 org.apache.httpcomponents httpclient 4.5.2 

Hier vindt u de laatste versie van het cxf-rt-frontend-jaxrs afhankelijkheid. U kunt ook naar deze link verwijzen voor de nieuwste versies van het org.apache.cxf: cxf-rt-transports-http-steiger artefacten. Eindelijk de nieuwste versie van httpclient vind je hier.

3. Resourceklassen en aanvraagtoewijzing

Laten we beginnen met het implementeren van een eenvoudig voorbeeld; we gaan onze REST API opzetten met twee bronnen Cursus en Leerling.

We beginnen eenvoudig en gaan gaandeweg naar een complexer voorbeeld.

3.1. De bronnen

Hier is de definitie van de Leerling resource klasse:

@XmlRootElement (name = "Student") openbare klas Student {privé int id; private String naam; // standaard getters en setters // standaard is gelijk aan en hashCode-implementaties}

Merk op dat we de @XmlRootElement annotatie om JAXB te vertellen dat instanties van deze klasse moeten worden gemarshaled naar XML.

Vervolgens komt de definitie van de Cursus resource klasse:

@XmlRootElement (name = "Course") openbare klas Cursus {privé int id; private String naam; privélijst studenten = nieuwe ArrayList (); private Student findById (int id) {voor (Student student: students) {if (student.getId () == id) {return student; }} retourneer null; }
 // standaard getters en setters // standaard is gelijk aan en hasCode-implementaties}

Laten we tot slot het CourseRepository - dat is de hoofdbron en dient als toegangspunt tot webservicebronnen:

@Path ("course") @Produces ("text / xml") openbare klas CourseRepository {privékaartcursussen = nieuwe HashMap (); // verzoek behandelingsmethoden privé Cursus findById (int id) {voor (Map.Entry cursus: courses.entrySet ()) {if (course.getKey () == id) {return course.getValue (); }} retourneer null; }}

Let op de mapping met de @Pad annotatie. De CourseRepository is hier de hoofdbron, dus het is toegewezen om alle URL's te verwerken die beginnen met Cursus.

De waarde van @Produceert annotatie wordt gebruikt om de server te vertellen objecten die zijn geretourneerd van methoden binnen deze klasse naar XML-documenten te converteren voordat ze naar clients worden verzonden. We gebruiken hier JAXB als standaard, aangezien er geen andere bindmechanismen zijn gespecificeerd.

3.2. Eenvoudige gegevensconfiguratie

Omdat dit een eenvoudige voorbeeldimplementatie is, gebruiken we gegevens in het geheugen in plaats van een volwaardige persistente oplossing.

Met dat in gedachten, laten we een aantal eenvoudige installatielogica implementeren om wat gegevens in het systeem te vullen:

{Student student1 = nieuwe student (); Student student2 = nieuwe student (); student1.setId (1); student1.setName ("Student A"); student2.setId (2); student2.setName ("Student B"); Lijst course1Students = nieuwe ArrayList (); course1Students.add (student1); course1Students.add (student2); Cursus course1 = nieuwe cursus (); Cursus course2 = nieuwe cursus (); course1.setId (1); course1.setName ("REST with Spring"); course1.setStudents (course1Students); course2.setId (2); course2.setName ("Leer Spring Security"); courses.put (1, course1); courses.put (2, course2); }

Methoden binnen deze klasse die voor HTTP-verzoeken zorgen, worden behandeld in de volgende subsectie.

3.3. De API - Request Mapping Methods

Laten we nu naar de implementatie van de eigenlijke REST API gaan.

We gaan API-bewerkingen toevoegen - met behulp van de @Pad annotatie - rechtstreeks in de POJO's van de bron.

Het is belangrijk om te begrijpen dat dit een significant verschil is met de aanpak in een typisch Spring-project - waar de API-bewerkingen zouden worden gedefinieerd in een controller, niet op de POJO zelf.

Laten we beginnen met toewijzingsmethoden die zijn gedefinieerd in het Cursus klasse:

@GET @Path ("{studentId}") openbare student getStudent (@PathParam ("studentId") int studentId) {return findById (studentId); }

Simpel gezegd, de methode wordt aangeroepen bij het afhandelen KRIJGEN verzoeken, aangeduid met de @KRIJGEN annotatie.

Merkte de eenvoudige syntaxis op van het in kaart brengen van het studentId padparameter van het HTTP-verzoek.

We gebruiken dan gewoon de findById helper-methode om het corresponderende Leerling voorbeeld.

De volgende methode behandelt POST verzoeken, aangegeven door de @POST annotatie, door het ontvangen Leerling bezwaar maken tegen de studenten lijst:

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

Dit levert een 200 OK reactie als het maken is gelukt, of 409 Conflict als een object met de ingezonden ID kaart bestaat al.

Merk ook op dat we de @Pad annotatie omdat de waarde een lege tekenreeks is.

De laatste methode zorgt voor VERWIJDEREN verzoeken. Het verwijdert een element uit de studenten lijst wiens ID kaart is de ontvangen padparameter en retourneert een antwoord met OK (200) status. Voor het geval er geen elementen zijn gekoppeld aan het opgegeven ID kaart, wat inhoudt dat er niets verwijderd hoeft te worden, deze methode retourneert een antwoord met Niet gevonden (404) status:

@DELETE @Path ("{studentId}") openbare reactie 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 (); }

Laten we verder gaan met het aanvragen van toewijzingsmethoden van de CourseRepository klasse.

Het volgende getCourse methode retourneert een Cursus object dat de waarde is van een item in het cursussen kaart waarvan de sleutel de ontvangen is Cursus id padparameter van een KRIJGEN verzoek. Intern verzendt de methode padparameters naar het findById helper-methode om zijn werk te doen.

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

Met de volgende methode wordt een bestaand item van het cursussen kaart, waar het lichaam van de ontvangen LEGGEN request is de invoerwaarde en de Cursus id parameter is de bijbehorende sleutel:

@PUT @Path ("courses / {courseId}") openbaar Antwoord updateCourse (@PathParam ("courseId") int courseId, cursuscursus) {Course existCourse = findById (courseId); if (bestaandeCourse == null) {return Response.status (Response.Status.NOT_FOUND) .build (); } if (bestaandeCourse.equals (cursus)) {return Response.notModified (). build (); } courses.put (courseId, course); return Response.ok (). build (); }

Dit updateCourse methode retourneert een antwoord met OK (200) status als de update is gelukt, verandert niets en retourneert een Niet-gemodificeerd (304) antwoord als de bestaande en geüploade objecten dezelfde veldwaarden hebben. In het geval een Cursus instantie met het gegeven ID kaart is niet gevonden in de cursussen map, retourneert de methode een antwoord met Niet gevonden (404) status.

De derde methode van deze root-resourceklasse verwerkt niet rechtstreeks een HTTP-verzoek. In plaats daarvan worden verzoeken gedelegeerd aan het Cursus klasse waar verzoeken worden afgehandeld door middel van overeenkomende methoden:

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

We hebben methoden getoond binnen de Cursus klasse die gedelegeerde verzoeken direct daarvoor verwerkt.

4. Server Eindpunt

Dit gedeelte richt zich op de constructie van een CXF-server, die wordt gebruikt voor het publiceren van de RESTful-webservice waarvan de bronnen in het voorgaande gedeelte worden weergegeven. De eerste stap is het instantiëren van een JAXRSServerFactoryBean object en stel de root-resourceklasse in:

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

Een resource provider moet vervolgens worden ingesteld op de factory-bean om de levenscyclus van de root-resourceklasse te beheren. We gebruiken de standaard singleton-resourceprovider die dezelfde resource-instantie retourneert voor elk verzoek:

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

We stellen ook een adres in om de URL aan te geven waar de webservice wordt gepubliceerd:

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

Nu de fabriekBean kan worden gebruikt om een ​​nieuw server die begint te luisteren naar inkomende verbindingen:

Server server = factoryBean.create ();

Alle bovenstaande code in deze sectie moet worden ingepakt in de hoofd methode:

public class RestfulServer {public static void main (String args []) genereert uitzondering {// bovenstaande codefragmenten}}

De aanroeping hiervan hoofd methode wordt gepresenteerd in sectie 6.

5. Testgevallen

In dit gedeelte worden testcases beschreven die worden gebruikt om de webservice die we eerder hebben gemaakt, te valideren. Deze tests valideren de resourcetoestanden van de service nadat ze hebben gereageerd op HTTP-verzoeken van de vier meest gebruikte methoden, namelijk KRIJGEN, POST, LEGGEN, en VERWIJDEREN.

5.1. Voorbereiding

Eerst worden twee statische velden gedeclareerd binnen de testklasse, genaamd Rustgevende test:

private static String BASE_URL = "// localhost: 8080 / baeldung / courses /"; privé statische CloseableHttpClient-client;

Voordat we tests uitvoeren, maken we een cliënt object, dat wordt gebruikt om te communiceren met de server en het daarna te vernietigen:

@BeforeClass openbare statische ongeldige createClient () {client = HttpClients.createDefault (); } @AfterClass openbare statische leegte closeClient () gooit IOException {client.close (); }

De cliënt instantie is nu klaar om te worden gebruikt door testgevallen.

5.2. KRIJGEN Verzoeken

In de testklasse definiëren we twee verzendmethoden KRIJGEN verzoeken aan de server waarop de webservice draait.

De eerste methode is om een Cursus instantie gezien zijn ID kaart in de bron:

privécursus getCourse (int courseOrder) gooit IOException {URL url = nieuwe URL (BASE_URL + courseOrder); InputStream input = url.openStream (); Cursuscursus = JAXB.unmarshal (nieuwe InputStreamReader (input), Course.class); terugkeer cursus; }

De tweede is om een Leerling instantie gegeven de ID kaarts van de cursus en student in de bron:

private Student getStudent (int courseOrder, int studentOrder) gooit IOException {URL url = nieuwe URL (BASE_URL + courseOrder + "/ students /" + studentOrder); InputStream input = url.openStream (); Student student = JAXB.unmarshal (nieuwe InputStreamReader (input), Student.class); terugkeer student; }

Deze methoden verzenden HTTP KRIJGEN verzoeken aan de serviceresource, en vervolgens unmarshal XML-antwoorden op instanties van de overeenkomstige klassen. Beide worden gebruikt om de status van serviceresources te verifiëren na uitvoering POST, LEGGEN, en VERWIJDEREN verzoeken.

5.3. POST Verzoeken

Deze paragraaf bevat twee testcases voor POST verzoeken, ter illustratie van de werking van de webservice bij het uploaden Leerling instantie leidt tot een conflict en wanneer het met succes is gemaakt.

In de eerste test gebruiken we een Leerling object ongemarkeerd van de conflict_student.xml bestand, gelegen op het klassenpad met de volgende inhoud:

 2 Leerling B 

Dit is hoe die inhoud wordt geconverteerd naar een POST verzoek body:

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

De Inhoudstype header is ingesteld om de server te vertellen dat het inhoudstype van het verzoek XML is:

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

Sinds het geüploade Leerling object bestaat al in het eerste Cursus we verwachten bijvoorbeeld dat de creatie mislukt en een reactie met Conflict (409) -status wordt geretourneerd. Het volgende codefragment verifieert de verwachting:

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

In de volgende test extraheren we de body van een HTTP-verzoek uit een bestand met de naam created_student.xml, ook op het klassenpad. Hier is de inhoud van het bestand:

 3 Leerling C 

Net als bij de vorige testcase, bouwen en voeren we een aanvraag uit, en verifiëren vervolgens of er met succes een nieuwe instantie is gemaakt:

HttpPost httpPost = nieuwe HttpPost (BASE_URL + "2 / students"); InputStream resourceStream = this.getClass (). GetClassLoader () .getResourceAsStream ("created_student.xml"); httpPost.setEntity (nieuwe InputStreamEntity (resourceStream)); httpPost.setHeader ("Content-Type", "text / xml"); HttpResponse response = client.execute (httpPost); assertEquals (200, response.getStatusLine (). getStatusCode ());

We kunnen nieuwe statussen van de webservicebron bevestigen:

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

Dit is wat het XML-antwoord op een verzoek om het nieuwe Leerling object ziet eruit als:

  3 Leerling C 

5.4. LEGGEN Verzoeken

Laten we beginnen met een ongeldig updateverzoek, waarbij de Cursus object dat wordt bijgewerkt, bestaat niet. Hier is de inhoud van de instantie die wordt gebruikt om een ​​niet-bestaande te vervangen Cursus object in de webservicebron:

 3 Apache CXF-ondersteuning voor RESTful 

Die inhoud wordt opgeslagen in een bestand met de naam niet_existent_course.xml op het klassenpad. Het wordt geëxtraheerd en vervolgens gebruikt om het lichaam van een LEGGEN verzoek via de onderstaande code:

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

De Inhoudstype header is ingesteld om de server te vertellen dat het inhoudstype van het verzoek XML is:

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

Omdat we opzettelijk een ongeldig verzoek hebben verzonden om een ​​niet-bestaand object bij te werken, is een Niet gevonden (404) reactie zal naar verwachting worden ontvangen. Het antwoord is gevalideerd:

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

In de tweede testcase voor LEGGEN verzoeken, we dienen een Cursus object met dezelfde veldwaarden. Aangezien er in dit geval niets verandert, verwachten we dat een reactie met Niet-gemodificeerd (304) status wordt geretourneerd. Het hele proces wordt geïllustreerd:

HttpPut httpPut = nieuwe HttpPut (BASE_URL + "1"); InputStream resourceStream = this.getClass (). GetClassLoader () .getResourceAsStream ("unchanged_course.xml"); httpPut.setEntity (nieuwe InputStreamEntity (resourceStream)); httpPut.setHeader ("Content-Type", "text / xml"); HttpResponse response = client.execute (httpPut); assertEquals (304, response.getStatusLine (). getStatusCode ());

Waar unchanged_course.xml is het bestand op het klassenpad dat de informatie bewaart die wordt gebruikt om te updaten. Hier is de inhoud:

 1 RUST met veer 

In de laatste demonstratie van LEGGEN verzoeken, voeren we een geldige update uit. Het volgende is de inhoud van de veranderde_cursus.xml bestand waarvan de inhoud wordt gebruikt om een Cursus instantie in de webservicebron:

 2 Apache CXF-ondersteuning voor RESTful 

Dit is hoe het verzoek wordt gebouwd en uitgevoerd:

HttpPut httpPut = nieuwe HttpPut (BASE_URL + "2"); InputStream resourceStream = this.getClass (). GetClassLoader () .getResourceAsStream ("modified_course.xml"); httpPut.setEntity (nieuwe InputStreamEntity (resourceStream)); httpPut.setHeader ("Content-Type", "text / xml");

Laten we een LEGGEN verzoek aan de server en valideer een succesvolle upload:

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

Laten we de nieuwe statussen van de webservicebron controleren:

Cursus cursus = getCourse (2); assertEquals (2, course.getId ()); assertEquals ("Apache CXF-ondersteuning voor RESTful", course.getName ());

Het volgende codefragment toont de inhoud van het XML-antwoord wanneer een GET-verzoek voor het eerder geüploade Cursus object wordt verzonden:

  2 Apache CXF-ondersteuning voor RESTful 

5.5. VERWIJDEREN Verzoeken

Laten we eerst proberen een niet-bestaand Leerling voorbeeld. De bewerking zou moeten mislukken en een overeenkomstig antwoord met Niet gevonden (404) status wordt verwacht:

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

In de tweede testcase voor VERWIJDEREN verzoeken, we maken, voeren en verifiëren een verzoek:

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

We verifiëren nieuwe statussen van de webservicebron met het volgende codefragment:

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

Vervolgens vermelden we het XML-antwoord dat wordt ontvangen na een verzoek voor het eerste Cursus object in de webservicebron:

  1 REST met Spring 2 Student B 

Het is duidelijk dat de eerste Leerling is succesvol verwijderd.

6. Testuitvoering

In sectie 4 wordt beschreven hoe u een Server instantie in de hoofd methode van de RestfulServer klasse.

De laatste stap om de server operationeel te maken, is door dat aan te roepen hoofd methode. Om dat te bereiken, is de Exec Maven-plug-in opgenomen en geconfigureerd in het Maven POM-bestand:

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

De laatste versie van deze plugin vind je via deze link.

Tijdens het compileren en verpakken van het artefact dat in deze tutorial wordt geïllustreerd, voert de Maven Surefire-plug-in automatisch alle tests uit die zijn ingesloten in klassen met namen die beginnen of eindigen met Test. Als dit het geval is, moet de plug-in worden geconfigureerd om die tests uit te sluiten:

 maven-surefire-plugin 2.19.1 ** / ServiceTest 

Met de bovenstaande configuratie, Servicetest is uitgesloten omdat het de naam is van de testklasse. U kunt elke naam voor die klasse kiezen, op voorwaarde dat de daarin opgenomen tests niet worden uitgevoerd door de Maven Surefire-plug-in voordat de server gereed is voor verbindingen.

Kijk hier voor de nieuwste versie van de Maven Surefire-plug-in.

Nu kunt u het exec: java doel om de RESTful-webserviceserver te starten en vervolgens de bovenstaande tests uit te voeren met behulp van een IDE. Op dezelfde manier kunt u de test starten door de opdracht uit te voeren mvn -Dtest = ServiceTest-test in een terminal.

7. Conclusie

Deze tutorial illustreerde het gebruik van Apache CXF als een JAX-RS-implementatie. Het liet zien hoe het raamwerk kan worden gebruikt om bronnen voor een RESTful-webservice te definiëren en om een ​​server te maken voor het publiceren van de service.

De implementatie van al deze voorbeelden en codefragmenten is te vinden in het GitHub-project.