البرمجة التفاعلية مع JAX-RS

مرحبا بالجميع!

في العام الماضي ، تم إطلاق دورة Java Enterprise Developer بنجاح ، ولدينا آخر مادة حول هذا الموضوع نود مشاركتها معك ، والتي تناقش استخدام النهج غير المتزامن والتدريج لتطوير التطبيقات سريعة الاستجابة.

دعنا نذهب.

تبدو البرمجة التفاعلية في البداية مثل اسم نموذج ناشئ ، لكنها في الواقع تشير إلى طريقة برمجة يتم فيها استخدام نهج موجه نحو الحدث للعمل مع تدفقات البيانات غير المتزامنة. بناءً على البيانات الحالية باستمرار ، تستجيب الأنظمة التفاعلية لها من خلال إجراء سلسلة من الأحداث.
تتبع البرمجة التفاعلية نمط التصميم “Observer” ، والذي يمكن تعريفه على النحو التالي: إذا تغيرت الحالة في كائن واحد ، فسيتم إعلام جميع الكائنات الأخرى وتحديثها وفقًا لذلك. لذلك ، بدلاً من استطلاع الأحداث للتغييرات ، يتم دفع الأحداث بشكل غير متزامن حتى يتمكن المراقبون من معالجتها. في هذا المثال ، المراقبون هم وظائف يتم تنفيذها عند إرسال الحدث. وتدفق البيانات المذكورة هو الملاحظة الفعلية.

تستخدم جميع اللغات والأطر تقريبًا هذا النهج في نظامها البيئي ، ولا تعد أحدث إصدارات جافا استثناءً. في هذه المقالة ، سأشرح كيف يمكن تطبيق البرمجة التفاعلية باستخدام أحدث إصدار من JAX-RS في وظائف Java EE 8 و Java 8.



بيان جت

يسرد جت مانيفستو أربعة جوانب أساسية يحتاج التطبيق إلى أن يكون أكثر مرونة ، مقرونًا بشكل فضفاض ، وسهل القياس ، وبالتالي قادر على أن يكون تفاعليًا. وينص على أن التطبيق يجب أن يكون متجاوبًا ومرنًا (وبالتالي قابلًا للتطوير) ومرنًا ومدفوعًا بالرسائل.

الهدف الأساسي هو تطبيق سريع الاستجابة. افترض أن هناك تطبيقًا يشارك فيه مؤشر ترابط كبير في معالجة طلبات المستخدم ، وبعد الانتهاء من العمل ، يرسل هذا الموضوع ردودًا إلى مقدمي الطلبات الأصليين. عندما يتلقى التطبيق طلبات أكثر مما يستطيع التعامل معها ، يصبح هذا الخيط اختناق ، ويفقد التطبيق استجابته السابقة. للحفاظ على الاستجابة ، يجب أن يكون التطبيق قابلاً للتطوير ومرنًا. يمكن اعتباره مستدامًا تطبيقًا يتميز بوظيفة الاسترداد التلقائي. في تجربة معظم المطورين ، فإن البنية المبنية على الرسائل فقط هي التي تسمح للتطبيق بأن يكون قابلاً للتطوير والمرونة والاستجابة.

تم تقديم البرمجة التفاعلية في Java 8 و Java EE 8. قدمت Java مفاهيم مثل CompletionStage وتطبيقها لـ CompletableFuture ، وبدأت Java في استخدام هذه الميزات في المواصفات مثل واجهة برمجة تطبيقات Reactive Client في JAX-RS.

واجهة برمجة تطبيقات عميل تفاعلي JAX-RS 2.1

دعونا نرى كيف يمكن استخدام البرمجة التفاعلية في تطبيقات Java EE 8. لفهم العملية ، تحتاج إلى بعض المعرفة الأساسية بواجهة برمجة تطبيقات Java EE API.

قدم JAX-RS 2.1 طريقة جديدة لإنشاء عميل REST مع دعم البرمجة التفاعلية. تطبيق Invoker الافتراضي المعروض في JAX-RS متزامن ، مما يعني أن العميل الذي تم إنشاؤه سيرسل مكالمة حظر إلى نقطة نهاية الخادم. يتم تقديم مثال على التنفيذ في القائمة 1.

الإدراج 1

 Response response = ClientBuilder.newClient() .target("http://localhost:8080/service-url") .request() .get(); 

بدءًا من الإصدار 2.0 ، يوفر JAX-RS الدعم لإنشاء Invoker غير المتزامن على واجهة برمجة تطبيقات العميل بمكالمة بسيطة لأسلوب async() ، كما هو موضح في القائمة 2.

قائمة 2

 Future<Response> response = ClientBuilder.newClient() .target("http://localhost:8080/service-url") .request() .async() .get(); 

يؤدي استخدام invoker غير متزامن على العميل إلى إرجاع نسخة Future من النوع javax.ws.rs.core.Response . يمكن أن يؤدي ذلك إلى استطلاع الرد ، من خلال مكالمة إلى future.get() ، أو تسجيل رد اتصال سيتم استدعاؤه عند توفر استجابة HTTP. كلا التطبيقين مناسبان للبرمجة غير المتزامنة ، ولكن الأمور عادة ما تكون معقدة إذا كنت ترغب في تجميع عمليات رد الاتصال أو إضافة حالات شرطية إلى هذه الحدود الدنيا للتنفيذ غير المتزامن.

يوفر JAX-RS 2.1 طريقة تفاعلية للتغلب على هذه المشاكل باستخدام واجهة برمجة تطبيقات JAX-RS Reactive Client الجديدة لبناء عميل. الأمر بسيط مثل استدعاء طريقة rx() أثناء بناء العميل. في القائمة 3 ، تُرجع طريقة rx() المستدعي التفاعلي الموجود في وقت تشغيل العميل ، ويعيد العميل استجابة من النوع CompletionStage.rx() ، والذي يسمح بالانتقال من المستثمر المتزامن إلى المستثمر غير المتزامن مع مكالمة بسيطة.

قائمة 3

 CompletionStage<Response> response = ClientBuilder.newClient() .target("http://localhost:8080/service-url") .request() .rx() .get(); 

CompletionStage<> هي واجهة جديدة تم تقديمها في Java 8. وهي تمثل عملية حسابية ، والتي يمكن أن تكون خطوة في إطار عملية حسابية أكبر ، كما يوحي الاسم. هذا هو ممثل تفاعلية Java 8 الوحيد الذي يصل إلى JAX-RS.
بعد تلقي مثيل الاستجابة ، يمكنني استدعاء AcceptAsync() ، حيث يمكنني تقديم جزء من التعليمات البرمجية سيتم تنفيذه بشكل غير متزامن عندما تصبح الاستجابة متاحة ، كما هو موضح في القائمة 4.

قائمة 4

 response.thenAcceptAsync(res -> { Temperature t = res.readEntity(Temperature.class); //do stuff with t }); 

إضافة تفاعل إلى نقطة نهاية REST

لا يقتصر النهج التفاعلي على جانب العميل في JAX-RS ؛ يمكن استخدامه أيضًا على جانب الخادم. على سبيل المثال ، سأقوم أولاً بإنشاء برنامج نصي بسيط حيث يمكنني طلب قائمة بمواقع وجهة واحدة. لكل موقف ، سوف أقوم بإجراء مكالمة منفصلة مع بيانات الموقع إلى نقطة أخرى للحصول على قيم درجة الحرارة. سيكون تفاعل الوجهات كما هو موضح في الشكل 1.


الشكل 1. التفاعل بين نقاط الوجهة

أولاً ، أحدد ببساطة نموذج المجال ، ثم الخدمات لكل نموذج. تعرض القائمة 5 كيفية تعريف فئة Forecast ، والتي تلتف فئتي Location ودرجة Temperature .

قائمة 5

 public class Temperature { private Double temperature; private String scale; // getters & setters } public class Location { String name; public Location() {} public Location(String name) { this.name = name; } // getters & setters } public class Forecast { private Location location; private Temperature temperature; public Forecast(Location location) { this.location = location; } public Forecast setTemperature( final Temperature temperature) { this.temperature = temperature; return this; } // getters } 

ServiceResponse قائمة التوقعات ، ServiceResponse تنفيذ فئة ServiceResponse في القائمة 6.

قائمة 6

 public class ServiceResponse { private long processingTime; private List<Forecast> forecasts = new ArrayList<>(); public void setProcessingTime(long processingTime) { this.processingTime = processingTime; } public ServiceResponse forecasts(List<Forecast> forecasts) { this.forecasts = forecasts; return this; } // getters } 

LocationResource الموضح في القائمة 7 ثلاثة مواقع عينة تم إرجاعها مع مسار /location .

قائمة 7

 @Path("/location") public class LocationResource { @GET @Produces(MediaType.APPLICATION_JSON) public Response getLocations() { List<Location> locations = new ArrayList<>(); locations.add(new Location("London")); locations.add(new Location("Istanbul")); locations.add(new Location("Prague")); return Response.ok(new GenericEntity<List<Location>>(locations){}).build(); } } 

إرجاع مصدر TemperatureResource الموضح في القائمة 8 قيمة درجة حرارة يتم إنشاؤها عشوائيًا بين 30 و 50 لموقع معين. تمت إضافة تأخير 500 مللي ثانية إلى التطبيق لمحاكاة قراءة المستشعر.

قائمة 8

 @Path("/temperature") public class TemperatureResource { @GET @Path("/{city}") @Produces(MediaType.APPLICATION_JSON) public Response getAverageTemperature(@PathParam("city") String cityName) { Temperature temperature = new Temperature(); temperature.setTemperature((double) (new Random().nextInt(20) + 30)); temperature.setScale("Celsius"); try { Thread.sleep(500); } catch (InterruptedException ignored) { ignored.printStackTrace(); } return Response.ok(temperature).build(); } } 

أولاً ، سوف أعرض تطبيق ForecastResource المتزامنة (انظر القائمة 9) ، التي تُرجع جميع المواقع. ثم ، بالنسبة لكل موقف ، يتصل بخدمة درجة الحرارة للحصول على القيم بالدرجات المئوية.

قائمة 9

 @Path("/forecast") public class ForecastResource { @Uri("location") private WebTarget locationTarget; @Uri("temperature/{city}") private WebTarget temperatureTarget; @GET @Produces(MediaType.APPLICATION_JSON) public Response getLocationsWithTemperature() { long startTime = System.currentTimeMillis(); ServiceResponse response = new ServiceResponse(); List<Location> locations = locationTarget .request() .get(new GenericType<List<Location>>(){}); locations.forEach(location -> { Temperature temperature = temperatureTarget .resolveTemplate("city", location.getName()) .request() .get(Temperature.class); response.getForecasts().add( new Forecast(location).setTemperature(temperature)); }); long endTime = System.currentTimeMillis(); response.setProcessingTime(endTime - startTime); return Response.ok(response).build(); } } 

عند طلب وجهة التوقعات على أنها /forecast ، ستحصل على ناتج مشابه لما هو موضح في القائمة 10. لاحظ أن وقت معالجة الطلب استغرق 1.533 مللي ثانية ، وهو أمر منطقي ، نظرًا لأن الطلب المتزامن لقيم درجة الحرارة من ثلاثة مواقع مختلفة يضيف ما يصل إلى 1.5 مللي ثانية

قائمة 10

 { "forecasts": [ { "location": { "name": "London" }, "temperature": { "scale": "Celsius", "temperature": 33 } }, { "location": { "name": "Istanbul" }, "temperature": { "scale": "Celsius", "temperature": 38 } }, { "location": { "name": "Prague" }, "temperature": { "scale": "Celsius", "temperature": 46 } } ], "processingTime": 1533 } 

حتى الآن ، كل شيء يسير حسب الخطة. حان الوقت لإدخال البرمجة التفاعلية على جانب الخادم ، حيث يمكن إجراء مكالمات لكل موقع بالتوازي بعد تلقي جميع المواقع. يمكن أن يؤدي ذلك إلى تحسين الدفق المتزامن الذي تم عرضه مسبقًا. يتم ذلك في القائمة 11 ، التي توضح تعريف إصدار خدمة التنبؤات التفاعلية.

قائمة 11

 @Path("/reactiveForecast") public class ForecastReactiveResource { @Uri("location") private WebTarget locationTarget; @Uri("temperature/{city}") private WebTarget temperatureTarget; @GET @Produces(MediaType.APPLICATION_JSON) public void getLocationsWithTemperature(@Suspended final AsyncResponse async) { long startTime = System.currentTimeMillis(); //   (stage)    CompletionStage<List<Location>> locationCS = locationTarget.request() .rx() .get(new GenericType<List<Location>>() {}); //      , //  ,   , //    CompletionStage final CompletionStage<List<Forecast>> forecastCS = locationCS.thenCompose(locations -> { //      //   ompletionStage List<CompletionStage<Forecast>> forecastList = //      //     locations.stream().map(location -> { //     //      //    final CompletionStage<Temperature> tempCS = temperatureTarget .resolveTemplate("city", location.getName()) .request() .rx() .get(Temperature.class); //   CompletableFuture,   //    //      return CompletableFuture.completedFuture( new Forecast(location)) .thenCombine(tempCS, Forecast::setTemperature); }).collect(Collectors.toList()); //    CompletableFuture, //     completable future //  return CompletableFuture.allOf( forecastList.toArray( new CompletableFuture[forecastList.size()])) .thenApply(v -> forecastList.stream() .map(CompletionStage::toCompletableFuture) .map(CompletableFuture::join) .collect(Collectors.toList())); }); //   ServiceResponse, //       //    . //   future    // forecastCS,    //      CompletableFuture.completedFuture( new ServiceResponse()) .thenCombine(forecastCS, ServiceResponse::forecasts) .whenCompleteAsync((response, throwable) -> { response.setProcessingTime( System.currentTimeMillis() - startTime); async.resume(response); }); } } 

قد يبدو التنفيذ التفاعلي معقدًا للوهلة الأولى ، ولكن بعد دراسة أكثر دقة ، ستلاحظ أنه بسيط للغاية. في تطبيق ForecastReactiveResource أقوم أولاً بإنشاء اتصال عميل لخدمات الموقع باستخدام واجهة برمجة تطبيقات عميل تفاعلية JAX-RS. كما ذكرت أعلاه ، هذه إضافة لـ Java EE 8 ، وتساعد على إنشاء مكالمة تفاعلية ببساطة باستخدام طريقة rx() .

أقوم الآن بإنشاء مرحلة جديدة استنادًا إلى الموقع لوضع قائمة بالتنبؤات. سيتم تخزينها كقائمة توقعات في مرحلة واحدة كبيرة من الإنجاز تسمى forecastCS . في النهاية ، سأقوم بإنشاء استجابة لمكالمة الخدمة باستخدام forecastCS خدمة العملاء فقط.

والآن ، دعونا نجمع التوقعات في شكل قائمة بمراحل الإنجاز المحددة في قائمة forecastList المتغيرات. لإنشاء مرحلة إكمال لكل توقعات ، أمرر البيانات حسب الموقع ، ثم أنشئ متغير tempCS ، مرة أخرى باستخدام JAX-RS Reactive Client API ، الذي يستدعي خدمة درجة الحرارة باسم المدينة. هنا ، أستخدم طريقة resolveTemplate() لبناء العميل ، وهذا يسمح لي بتمرير اسم المدينة إلى المجمع كمعلمة.

كخطوة أخيرة في البث ، أقوم بإجراء مكالمة إلى CompletableFuture.completedFuture() ، لتمرير مثيل Forecast الجديد كمعلمة. أقوم بدمج هذا المستقبل مع مرحلة tempCS حتى أحصل على قيمة درجة حرارة للمواقع المراقبة.

تحول طريقة CompletableFuture.allOf() في القائمة 11 قائمة مرحلة الإكمال إلى forecastCS . يؤدي تنفيذ هذه الخطوة إلى إرجاع نسخة مستقبلية كبيرة قابلة للإكمال عند اكتمال جميع الكائنات المستقبلية القابلة للإكمال.

تعد استجابة الخدمة ServiceResponse لفئة ServiceResponse ، لذلك أقوم بإنشاء مستقبل مكتمل ، ثم أقوم بدمج مرحلة إكمال خدمة forecastCS ServiceResponse مع قائمة من التوقعات وحساب وقت استجابة الخدمة.

بالطبع ، تفرض البرمجة التفاعلية جانب الخادم فقط للعمل بشكل غير متزامن ؛ سيتم حظر جانب العميل حتى يرسل الخادم استجابة إلى الطالب. للتغلب على هذه المشكلة ، يمكن استخدام الأحداث المرسلة من الخادم (SSEs) لإرسال استجابة جزئيًا بمجرد توفرها بحيث يتم نقل قيم درجة الحرارة لكل موقع إلى العميل واحدًا تلو الآخر. سيكون ناتج ForecastReactiveResource مشابهًا لما هو معروض في القائمة 12. كما هو موضح في الإخراج ، فإن وقت المعالجة هو 515 مللي ثانية ، وهو وقت تشغيل مثالي للحصول على قيم درجة الحرارة من موقع واحد.

قائمة 12

 { "forecasts": [ { "location": { "name": "London" }, "temperature": { "scale": "Celsius", "temperature": 49 } }, { "location": { "name": "Istanbul" }, "temperature": { "scale": "Celsius", "temperature": 32 } }, { "location": { "name": "Prague" }, "temperature": { "scale": "Celsius", "temperature": 45 } } ], "processingTime": 515 } 

الخلاصة

في الأمثلة الواردة في هذه المقالة ، عرضت أولاً طريقة متزامنة للحصول على توقعات باستخدام خدمات الموقع ودرجة الحرارة. ثم ، انتقلت إلى النهج التفاعلي بحيث تتم المعالجة غير المتزامنة بين مكالمات الخدمة. عند استخدام JAX-RS Reactive Client API في Java EE 8 ، جنبًا إلى جنب مع فئات CompletionStage و CompletableFuture المتوفرة في Java 8 ، فإن قوة المعالجة غير المتزامنة تتعطل بفضل البرمجة التفاعلية.

البرمجة التفاعلية أكثر من مجرد تطبيق نموذج غير متزامن من نموذج متزامن ؛ كما يبسط العمل مع مفاهيم مثل مرحلة التعشيش. كلما زاد استخدامه ، كان من الأسهل إدارة البرامج النصية المعقدة في البرمجة المتوازية.

النهاية

شكرا لكم على اهتمامكم. كما هو الحال دائمًا ، نحن في انتظار تعليقاتك وأسئلتك.

Source: https://habr.com/ru/post/ar424031/


All Articles