תמיכה באפצ'י CXF בשירותי אינטרנט RESTful

1. סקירה כללית

הדרכה זו מציגה את Apache CXF כמסגרת התואמת את תקן JAX-RS, המגדיר תמיכה במערכת האקולוגית של Java בתבנית האדריכלית REPResentational State Transfer (REST).

באופן ספציפי, הוא מתאר שלב אחר שלב כיצד לבנות ולפרסם שירות אינטרנט RESTful, וכיצד לכתוב בדיקות יחידה לאימות שירות.

זו השלישית בסדרה על אפאצ'י CXF; הראשון מתמקד בשימוש ב- CXF כמימוש תואם לחלוטין של JAX-WS. המאמר השני מספק מדריך כיצד להשתמש ב- CXF עם Spring.

2. תלות Maven

התלות הנדרשת הראשונה היא org.apache.cxf: cxf- rt -frontend-jaxrs. חפץ זה מספק ממשקי API של JAX-RS כמו גם יישום CXF:

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

במדריך זה אנו משתמשים ב- CXF כדי ליצור שרת נקודת קצה כדי לפרסם שירות אינטרנט במקום להשתמש במיכל servlet. לכן, יש לכלול את התלות הבאה בקובץ MOM POM:

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

לבסוף, נוסיף את ספריית HttpClient כדי להקל על בדיקות היחידה:

 org.apache.httpcomponents httpclient 4.5.2 

כאן תוכל למצוא את הגרסה האחרונה של ה- cxf-rt-frontend-jaxrs תלות. מומלץ גם להפנות לקישור זה לגירסאות העדכניות ביותר של ה- org.apache.cxf: cxf-rt-transports-http-jetty חפצים. לבסוף, הגרסה האחרונה של httpclient ניתן למצוא כאן.

3. שיעורי משאבים ומיפוי בקשות

נתחיל ליישם דוגמה פשוטה; אנו מקימים את ה- REST API שלנו עם שני משאבים קוּרס ו סטוּדֶנט.

נתחיל פשוט ונתקדם לקראת דוגמה מורכבת יותר תוך כדי.

3.1. המשאבים

להלן ההגדרה של סטוּדֶנט מחלקת משאבים:

@XmlRootElement (name = "סטודנט") תלמיד בכיתה ציבורית {id id פרטי; שם מחרוזת פרטי; // סטנדרטים וקובעים סטנדרטיים // סטנדרטיים שווים ויישומי hashCode}

שימו לב שאנחנו משתמשים ב- @XmlRootElement ביאור כדי לומר ל- JAXB כי יש לנקוט במקרים של מחלקה זו ל- XML.

לאחר מכן, באה ההגדרה של קוּרס מחלקת משאבים:

@XmlRootElement (name = "Course") קורס ציבורי קורס {id id פרטי; שם מחרוזת פרטי; סטודנטים לרשימה פרטית = ArrayList חדש (); סטודנט פרטי findById (מזהה int) {עבור (סטודנט סטודנט: סטודנטים) {if (student.getId () == id) {תלמיד חוזר; }} להחזיר null; }
 // תקנים וקובעים סטנדרטיים // תקנים שווים והטמעות hasCode}

לבסוף, בואו ליישם את מאגר קורס - שהוא משאב הבסיס ומשמש כנקודת הכניסה למשאבי שירות האינטרנט:

@Path ("קורס") @Produces ("טקסט / xml") מעמד ציבורי CourseRepository {קורסים פרטיים במפה = HashMap חדש (); // בקש שיטות טיפול פרטי findById (id מזהה) {עבור (קורס Map.Entry: courses.entrySet ()) {if (course.getKey () == id) {return kurs.getValue (); }} להחזיר null; }}

שימו לב למיפוי עם @נָתִיב ביאור. ה מאגר קורס הוא משאב הבסיס כאן, ולכן הוא ממופה לטיפול בכל כתובות ה- URL החל מ קוּרס.

הערך של @Produces ביאור משמש כדי לשרת לשרת להמיר אובייקטים שהוחזרו משיטות במחלקה זו למסמכי XML לפני שליחתם ללקוחות. אנו משתמשים ב- JAXB כאן כברירת מחדל מכיוון שלא צוינו מנגנוני כריכה אחרים.

3.2. הגדרת נתונים פשוטה

מכיוון שמדובר ביישום פשוט לדוגמא, אנו משתמשים בנתוני זיכרון במקום בפתרון מתמיד מלא.

עם זאת, בואו נשתמש בהיגיון התקנה פשוט לאכלוס נתונים מסוימים במערכת:

{סטודנט סטודנט 1 = סטודנט חדש (); סטודנט סטודנט 2 = סטודנט חדש (); student1.setId (1); student1.setName ("סטודנט א"); student2.setId (2); student2.setName ("סטודנט B"); רשימת course1Students = ArrayList חדש (); course1Students.add (סטודנט 1); course1Students.add (סטודנט 2); קורס קורס 1 = קורס חדש (); קורס קורס 2 = קורס חדש (); קורס1.setId (1); course1.setName ("לנוח עם אביב"); course1.setStudents (course1Students); course2.setId (2); course2.setName ("למד אבטחת אביב"); קורסים.פלט (1, קורס 1); קורסים.פלט (2, קורס 2); }

שיטות בתוך מחלקה זו המטפלות בבקשות HTTP מכוסות בסעיף המשנה הבא.

3.3. ה- API - בקש שיטות מיפוי

עכשיו, בוא נעבור ליישום ממשק ה- REST ממש.

אנו נתחיל להוסיף פעולות API - באמצעות ה- @נָתִיב ביאור - ממש במשאבי ה- POJO.

חשוב להבין שזה הבדל משמעותי מהגישה בפרויקט אביב אופייני - שבו פעולות ה- API יוגדרו בבקר, ולא ב- POJO עצמו.

נתחיל בשיטות מיפוי המוגדרות בתוך ה- קוּרס מעמד:

@GET @Path ("{studentId}") getStudent ציבורי של סטודנטים (@PathParam ("studentId") int studentId) {return findById (studentId); }

במילים פשוטות, השיטה מופעלת בעת הטיפול לקבל בקשות, המסומנות על ידי @לקבל ביאור.

הבחין בתחביר הפשוט של מיפוי ה- תעודת סטודנט פרמטר נתיב מבקשת HTTP.

לאחר מכן אנו פשוט משתמשים ב- findById שיטת עוזר להחזרת המקביל סטוּדֶנט למשל.

השיטה הבאה מטפלת הודעה בקשות, המצוינות על ידי @הודעה ביאור, על ידי הוספת התקבל סטוּדֶנט התנגדות ל סטודנטים רשימה:

@POST @ Path ("") תגובה ציבורית createStudent (סטודנט סטודנט) {עבור (רכיב סטודנט: סטודנטים) {if (element.getId () == student.getId () {return Response.status (Response.Status.CONFLICT) .build ();}} students.add (student); return Response.ok (student) .build ();}

זה מחזיר א 200 בסדר תגובה אם פעולת היצירה הצליחה, או 409 סכסוך אם חפץ שהוגש תְעוּדַת זֶהוּת כבר קיים.

שימו לב גם שאנחנו יכולים לדלג על ה- @נָתִיב ביאור מכיוון שערכו הוא מחרוזת ריקה.

השיטה האחרונה דואגת לִמְחוֹק בקשות. זה מסיר אלמנט מה- סטודנטים רשימה של מי תְעוּדַת זֶהוּת הוא פרמטר הנתיב שהתקבל ומחזיר תגובה עם בסדר (200) סטטוס. במקרה שאין אלמנטים המשויכים למפורט תְעוּדַת זֶהוּת, מה שמרמז שאין מה להסיר, שיטה זו מחזירה תגובה עם לא נמצא (404) סטטוס:

@DELETE @Path ("{studentId}") תגובה ציבורית deleteStudent (@PathParam ("studentId") int studentId) {סטודנט סטודנט = findById (studentId); אם (סטודנט == null) {return Response.status (Response.Status.NOT_FOUND) .build (); } סטודנטים. הסר (סטודנט); החזר Response.ok (). build (); }

נעבור לבקש שיטות מיפוי של ה- מאגר קורס מעמד.

הבאים getCourse שיטה מחזירה א קוּרס אובייקט שהוא הערך של ערך ב- קורסים מפה שהמפתח שלה הוא המקבל מזהה קורס פרמטר נתיב של a לקבל בַּקָשָׁה. באופן פנימי, השיטה שולחת פרמטרים של נתיבים אל ה- findById שיטת עוזר לעשות את עבודתה.

@GET @Path ("קורסים / {courseId}") קורס קורס ציבורי getCourse (@PathParam ("courseId") int courseId) {return findById (courseId); }

השיטה הבאה מעדכנת רשומה קיימת של ה- קורסים מפה, שם גופה של התקבלה לָשִׂים הבקשה היא ערך הכניסה וה- מזהה קורס הפרמטר הוא המפתח המשויך:

@PUT @Path ("קורסים / {courseId}") תגובה ציבורית publicCourse (@PathParam ("courseId") int courseId, קורס קורס) {Course existingCourse = findById (courseId); אם (existingCourse == null) {return Response.status (Response.Status.NOT_FOUND) .build (); } אם (existingCourse.equals (קורס)) {return Response.notModified (). build (); } קורסים.פלט (קורסId, קורס); החזר Response.ok (). build (); }

זֶה updateCourse השיטה מחזירה תגובה עם בסדר (200) סטטוס אם העדכון הצליח, אינו משנה דבר ומחזיר א לא שונה (304) תגובה אם האובייקטים הקיימים והעלויים הם בעלי ערכי שדה זהים. במקרה א קוּרס למשל עם הנתון תְעוּדַת זֶהוּת לא נמצא ב- קורסים מפה, השיטה מחזירה תגובה עם לא נמצא (404) סטטוס.

השיטה השלישית של מחלקת משאבי שורש זו אינה מטפלת ישירות בשום בקשת HTTP. במקום זאת, הוא מאציל בקשות ל קוּרס מחלקה שבה בקשות מטופלות בשיטות התאמה:

@Path ("קורסים / {courseId} / סטודנטים") pathSite קורס קורס ציבורי (@PathParam ("courseId") int courseId) {return findById (courseId); }

הראינו שיטות בתוך ה- קוּרס מחלקה שמעבדת בקשות שהועברו ממש לפני.

4. שרת נקודת קצה

חלק זה מתמקד בבניית שרת CXF המשמש לפרסום שירות האינטרנט RESTful שמשאביו מתוארים בסעיף הקודם. הצעד הראשון הוא להתקין א JAXRSServerFactoryBean התנגד והגדר את מחלקת משאבי הבסיס:

JAXRSServerFactoryBean factoryBean = JAXRSServerFactoryBean חדש (); factoryBean.setResourceClasses (CourseRepository.class);

לאחר מכן צריך להגדיר ספק משאבים על שעועית המפעל כדי לנהל את מחזור החיים של מחלקת משאבי השורש. אנו משתמשים בספק ברירת המחדל של משאבי סינגלטון המחזיר את אותו מופע משאב לכל בקשה:

factoryBean.setResourceProvider (SingletonResourceProvider חדש (CourseRepository חדש ()));

קבענו גם כתובת שתציין את כתובת האתר שבה מתפרסם שירות האינטרנט:

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

עכשיו ה factoryBean יכול לשמש ליצירת חדש שרת שיתחיל להאזין לחיבורים נכנסים:

שרת שרתים = factoryBean.create ();

כל הקוד שלמעלה בסעיף זה צריך להיות עטוף ב- רָאשִׁי שיטה:

class class RestfulServer {public static void main (String args []) זורק Exception {// קטעי קוד המוצגים לעיל}}

הקריאה לכך רָאשִׁי השיטה מוצגת בסעיף 6.

5. מקרי מבחן

חלק זה מתאר מקרי בדיקה ששימשו לאימות שירות האינטרנט שיצרנו קודם. בדיקות אלה מאמתות מצבי משאבים של השירות לאחר מענה לבקשות HTTP של ארבע השיטות הנפוצות ביותר, כלומר לקבל, הודעה, לָשִׂים, ו לִמְחוֹק.

5.1. הכנה

ראשית, שני שדות סטטיים מוכרזים במסגרת מחלקת הבדיקה, בשם RestfulTest:

מחרוזת סטטית פרטית BASE_URL = "// localhost: 8080 / baeldung / courses /"; לקוח סטטי פרטי CloseableHttpClient;

לפני הפעלת מבחנים אנו יוצרים a לָקוּחַ אובייקט, המשמש לתקשורת עם השרת ולהשמדתו לאחר מכן:

@BeforeClass חלל סטטי ציבורי createClient () {client = HttpClients.createDefault (); } @AfterClass ריק סטטי ציבורי closeClient () זורק IOException {client.close (); }

ה לָקוּחַ מופע מוכן כעת לשימוש על ידי מקרי בדיקה.

5.2. לקבל בקשות

בשיעור הבדיקה אנו מגדירים שתי שיטות למשלוח לקבל בקשות לשרת המפעיל את שירות האינטרנט.

השיטה הראשונה היא להשיג א קוּרס מופע בהתחשב שלו תְעוּדַת זֶהוּת במשאב:

קורס קורס פרטי GetCourse (int courseOrder) זורק IOException {URL url = URL חדש (BASE_URL + קורס סדר); קלט InputStream = url.openStream (); קורס קורס = JAXB.unmarshal (InputStreamReader חדש (קלט), Course.class); קורס חזרה; }

השנייה היא להשיג א סטוּדֶנט מקרה בהינתן תְעוּדַת זֶהוּתשל הקורס והתלמיד במשאב:

סטודנט פרטי GetStudent (int courseOrder, int studentOrder) זורק IOException {URL url = URL חדש (BASE_URL + קורס הזמנה + "/ סטודנטים /" + StudentOrder); קלט InputStream = url.openStream (); סטודנט סטודנט = JAXB.unmarshal (InputStreamReader חדש (קלט), Student.class); תלמיד חוזר; }

שיטות אלה שולחות HTTP לקבל בקשות למשאב השירות, ואז לבטל תגובות XML למופעים של המחלקות המתאימות. שניהם משמשים לאימות מצבי משאבי שירות לאחר הביצוע הודעה, לָשִׂים, ו לִמְחוֹק בקשות.

5.3. הודעה בקשות

סעיף קטן זה כולל שני מקרי מבחן עבור הודעה מבקש, הממחיש את פעולות שירות האינטרנט בעת ההעלאה סטוּדֶנט מופע מוביל לסכסוך וכאשר הוא נוצר בהצלחה.

במבחן הראשון אנו משתמשים ב- סטוּדֶנט אובייקט שלא נבדק מה- conflict_student.xml קובץ, הממוקם על מסלול הכיתה עם התוכן הבא:

 2 סטודנט ב ' 

כך מומר תוכן זה ל- הודעה גוף בקשה:

HttpPost httpPost = HttpPost חדש (BASE_URL + "1 / סטודנטים"); InputStream resourceStream = this.getClass (). GetClassLoader () .getResourceAsStream ("conflict_student.xml"); httpPost.setEntity (InputStreamEntity חדש (resourceStream));

ה סוג תוכן כותרת מוגדרת לומר לשרת שסוג התוכן של הבקשה הוא XML:

httpPost.setHeader ("סוג תוכן", "טקסט / xml");

מאז שהועלה סטוּדֶנט אובייקט כבר קיים בראשון קוּרס למשל, אנו מצפים שהיצירה תיכשל ותגובה עם סְתִירָה (409) סטטוס מוחזר. קטע הקוד הבא מאמת את הציפייה:

תגובה HttpResponse = client.execute (httpPost); assertEquals (409, response.getStatusLine (). getStatusCode ());

במבחן הבא אנו מחלצים את גוף בקשת HTTP מקובץ בשם created_student.xml, גם על מסלול הכיתה. הנה תוכן הקובץ:

 3 תלמיד ג 

בדומה למקרה הבדיקה הקודם, אנו בונים ומבצעים בקשה, ואז מוודאים כי מופע חדש נוצר בהצלחה:

HttpPost httpPost = HttpPost חדש (BASE_URL + "2 / סטודנטים"); InputStream resourceStream = this.getClass (). GetClassLoader () .getResourceAsStream ("created_student.xml"); httpPost.setEntity (InputStreamEntity חדש (resourceStream)); httpPost.setHeader ("סוג תוכן", "טקסט / xml"); תגובה HttpResponse = client.execute (httpPost); assertEquals (200, response.getStatusLine (). getStatusCode ());

אנו עשויים לאשר מצבים חדשים של משאב שירות האינטרנט:

סטודנט סטודנט = getStudent (2, 3); assertEquals (3, student.getId ()); assertEquals ("סטודנט C", student.getName ());

זה מה שתגובת ה- XML ​​לבקשה לחדש סטוּדֶנט האובייקט נראה כמו:

  3 תלמיד ג 

5.4. לָשִׂים בקשות

נתחיל בבקשת עדכון לא חוקית, שם קוּרס האובייקט המתעדכן אינו קיים. להלן תוכן המופע המשמש להחלפה שאינה קיימת קוּרס אובייקט במשאב שירות האינטרנט:

 3 תמיכה באפצ'י CXF עבור RESTful 

תוכן זה נשמר בקובץ שנקרא non_existent_course.xml על מסלול הכיתה. הוא מופק ואז משמש לאכלוס גופו של א לָשִׂים בקשה לפי הקוד שלהלן:

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

ה סוג תוכן כותרת מוגדרת לומר לשרת שסוג התוכן של הבקשה הוא XML:

httpPut.setHeader ("סוג תוכן", "טקסט / xml");

מכיוון ששלחנו בכוונה בקשה לא חוקית לעדכון אובייקט שאינו קיים, א לא נמצא (404) תגובה צפויה להתקבל. התגובה אומתה:

תגובה HttpResponse = client.execute (httpPut); assertEquals (404, response.getStatusLine (). getStatusCode ());

במקרה הבדיקה השני עבור לָשִׂים בקשות, אנו מגישים א קוּרס אובייקט עם אותם ערכי שדה. מכיוון ששום דבר לא השתנה במקרה זה, אנו מצפים שתגובה עם לא שונה (304) סטטוס מוחזר. התהליך כולו מודגם:

HttpPut httpPut = HttpPut חדש (BASE_URL + "1"); InputStream resourceStream = this.getClass (). GetClassLoader () .getResourceAsStream ("unchanged_course.xml"); httpPut.setEntity (InputStreamEntity חדש (resourceStream)); httpPut.setHeader ("סוג תוכן", "טקסט / xml"); תגובה HttpResponse = client.execute (httpPut); assertEquals (304, response.getStatusLine (). getStatusCode ());

איפה unchanged_course.xml הוא הקובץ השומר על שביל הכיתה המשמש לעדכון. להלן תוכנו:

 מנוחה אחת עם אביב 

בהפגנה האחרונה של לָשִׂים בקשות, אנו מבצעים עדכון חוקי. להלן תוכן ה- changed_course.xml קובץ שתוכנו משמש לעדכון a קוּרס מופע במשאב שירות האינטרנט:

 2 אפאצ'י CXF תמיכה ב- RESTful 

כך נבנית ומבוצעת הבקשה:

HttpPut httpPut = HttpPut חדש (BASE_URL + "2"); InputStream resourceStream = this.getClass (). GetClassLoader () .getResourceAsStream ("changed_course.xml"); httpPut.setEntity (InputStreamEntity חדש (resourceStream)); httpPut.setHeader ("סוג תוכן", "טקסט / xml");

בואו נאמת א לָשִׂים בקש לשרת ואמת העלאה מוצלחת:

תגובה HttpResponse = client.execute (httpPut); assertEquals (200, response.getStatusLine (). getStatusCode ());

בואו נאמת את המצבים החדשים של משאב שירות האינטרנט:

קורס קורס = getCourse (2); assertEquals (2, course.getId ()); assertEquals ("תמיכה ב- Apache CXF עבור RESTful", course.getName ());

קטע הקוד הבא מציג את תוכן תגובת ה- XML ​​כאשר בקשת GET עבור העלאה הקודמת קוּרס האובייקט נשלח:

  2 אפאצ'י CXF תמיכה ב- RESTful 

5.5. לִמְחוֹק בקשות

ראשית, בואו ננסה למחוק לא קיים סטוּדֶנט למשל. הפעולה אמורה להיכשל ותגובה מתאימה עם לא נמצא צפוי סטטוס (404):

HttpDelete httpDelete = HttpDelete חדש (BASE_URL + "1 / סטודנטים / 3"); תגובה HttpResponse = client.execute (httpDelete); assertEquals (404, response.getStatusLine (). getStatusCode ());

במקרה הבדיקה השני עבור לִמְחוֹק בקשות, אנו יוצרים, מבצעים ומאמתים בקשה:

HttpDelete httpDelete = HttpDelete חדש (BASE_URL + "1 / סטודנטים / 1"); תגובה HttpResponse = client.execute (httpDelete); assertEquals (200, response.getStatusLine (). getStatusCode ());

אנו מאמתים מצבים חדשים של משאב שירות האינטרנט באמצעות קטע הקוד הבא:

קורס קורס = getCourse (1); assertEquals (1, course.getStudents (). size ()); assertEquals (2, course.getStudents (). get (0) .getId ()); assertEquals ("סטודנט B", course.getStudents (). get (0) .getName ());

לאחר מכן, אנו מפרטים את תגובת ה- XML ​​שמתקבלת לאחר בקשה לראשונה קוּרס אובייקט במשאב שירות האינטרנט:

  מנוחה אחת עם אביב 2 סטודנט ב ' 

ברור כי הראשון סטוּדֶנט הוסר בהצלחה.

6. ביצוע מבחן

סעיף 4 תיאר כיצד ליצור ולהשמיד א שרת למשל ב רָאשִׁי שיטת ה- RestfulServer מעמד.

השלב האחרון להפעלת השרת הוא להפעיל את זה רָאשִׁי שיטה. על מנת להשיג זאת, תוסף Exec Maven נכלל ומוגדר בקובץ Maven POM:

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

הגרסה האחרונה של תוסף זה ניתן למצוא באמצעות קישור זה.

בתהליך הידור והאריזה של החפץ שמודגם במדריך זה, התוסף Maven Surefire מבצע באופן אוטומטי את כל הבדיקות הכלולות בשיעורים עם שמות שמתחילים או מסתיימים מִבְחָן. אם זה המקרה, יש להגדיר את התוסף כך שלא יכלול את הבדיקות הללו:

 תוסף maven-surefire 2.19.1 ** / ServiceTest 

עם התצורה לעיל, ServiceTest אינו נכלל מכיוון שזהו שם כיתת המבחן. אתה יכול לבחור כל שם עבור אותה מחלקה, בתנאי שבדיקות הכלולות בו אינן מנוהלות על ידי התוסף Maven Surefire לפני שהשרת מוכן לחיבורים.

לקבלת הגרסה האחרונה של תוסף Maven Surefire, אנא בדוק כאן.

עכשיו אתה יכול לבצע את exec: java המטרה להפעיל את שרת שירותי האינטרנט RESTful ואז להריץ את הבדיקות הנ"ל באמצעות IDE. באופן שווה, אתה יכול להתחיל את הבדיקה על ידי ביצוע הפקודה mvn -Dtest = בדיקת ServiceTest בטרמינל.

7. מסקנה

מדריך זה המחיש את השימוש באפצ'י CXF כהטמעת JAX-RS. היא הדגימה כיצד ניתן להשתמש במסגרת להגדרת משאבים לשירות אינטרנט RESTful וליצירת שרת לפרסום השירות.

ניתן למצוא את היישום של כל הדוגמאות וקטעי הקוד בפרויקט GitHub.