Skip to content

13. [Cours]: عرض قاعدة بيانات على الويب باستخدام Spring MVC

الكلمات الرئيسية: بنية متعددة الطبقات، Spring، حقن التبعيات، خدمة الويب / jSON، العميل / الخادم

13.1. Support

 

ستجد مشاريع هذا الفصل في المجلد [support / chap-13]. يتيح البرنامج النصي SQL [dbintrospringdata.sql] إنشاء قاعدة البيانات MySQL اللازمة للاختبارات.

13.2. مكانة Spring MVC في تطبيق الويب

لنحدد مكان Spring MVC في تطوير تطبيق ويب. في أغلب الأحيان، سيتم بناء هذا التطبيق على بنية متعددة الطبقات مثل التالية:

  • الطبقة [Web] هي الطبقة التي تتعامل مع مستخدم تطبيق الويب. يتفاعل المستخدم مع تطبيق الويب من خلال صفحات الويب التي يتم عرضها بواسطة متصفح. يقع Spring MVC في هذه الطبقة وفقط في هذه الطبقة؛
  • تنفذ الطبقة [métier] قواعد إدارة التطبيق، مثل حساب الراتب أو الفاتورة. تستخدم هذه الطبقة البيانات الواردة من المستخدم عبر الطبقة [Web] ومن SGBD عبر الطبقة [DAO
  • تدير الطبقة [DAO] (كائنات الوصول إلى البيانات)، والطبقة [ORM] (مخطط العلاقات بين الكائنات)، وبرنامج التشغيل JDBC الوصول إلى بيانات SGBD. تشكل الطبقة [ORM] جسرًا بين الكائنات التي تعالجها الطبقة [DAO] والصفوف والأعمدة في جداول قاعدة البيانات العلائقية. تسمح المواصفة JPA (Java Persistence API) بالتجريد من ORM المستخدم إذا كان هذا الأخير يطبق هذه المواصفات. سيكون هذا هو الحال هنا وسنطلق من الآن فصاعدًا على الطبقة ORM اسم الطبقة JPA؛
  • يتم دمج الطبقات بواسطة إطار عمل Spring؛

13.3. نموذج تطوير Spring MVC

يقوم Spring MVC بتنفيذ نموذج الهندسة المعمارية المسمى MVC (النموذج – العرض – وحدة التحكم) بالطريقة التالية:

تتم معالجة طلب العميل على النحو التالي:

  1. الطلب - تكون URL المطلوبة بالشكل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... يستخدم [Front Controller] ملف تكوين أو تعليقات Java لتوجيه الطلب إلى وحدة التحكم الصحيحة والإجراء الصحيح داخل وحدة التحكم هذه. وللقيام بذلك، يستخدم الحقل [Action] في URL. أما باقي أجزاء URL و [/param1/param2/...] فهي تتكون من معلمات اختيارية سيتم تمريرها إلى الإجراء. يكون C في MVC هنا هو السلسلة [Front Controller, Contrôleur, Action]. إذا لم يتمكن أي وحدة تحكم من معالجة الإجراء المطلوب، فسيرد خادم الويب بأن URL المطلوب لم يتم العثور عليه.
  2. معالجة
    • يمكن للإجراء المختار استخدام المعلمات parami التي أرسلها إليه [Front Controller]. وقد تأتي هذه المعلمات من عدة مصادر:
    • مسار [/param1/param2/...] الخاص بـ URL،
    • من المعلمات [p1=v1&p2=v2] الخاصة بـ URL,
    • من المعلمات التي أرسلها المتصفح مع طلبه؛
    • في معالجة طلب المستخدم، قد تحتاج العملية إلى الطبقة [métier] [2b]. بمجرد معالجة طلب العميل، يمكن أن يستدعي هذا الطلب استجابات متنوعة. ومن الأمثلة النموذجية على ذلك:
    • صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
    • صفحة تأكيد في الحالات الأخرى
    • تطلب الإجراء عرض طريقة عرض معينة [3]. ستعرض طريقة العرض هذه البيانات التي نسميها نموذج طريقة العرض. هذا هو M في MVC. ستقوم الإجراء بإنشاء هذا النموذج M [2c] وتطلب عرض طريقة عرض V [3]؛
  3. الاستجابة - تستخدم طريقة العرض V المختارة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من الاستجابة HTML التي يجب أن ترسلها إلى العميل ثم ترسل هذه الاستجابة.

بالنسبة لخدمة الويب / jSON، يتم تعديل البنية السابقة بشكل طفيف:

  • في [4a]، يتم تحويل النموذج الذي هو فئة Java إلى سلسلة jSON بواسطة مكتبة jSON؛
  • في [4b]، يتم إرسال هذه السلسلة jSON إلى المتصفح؛

الآن، دعونا نوضح العلاقة بين بنية الويب MVC وبنية الطبقات. وفقًا للتعريف الذي نضعه للنموذج، قد يكون هذان المفهومان مرتبطين أو غير مرتبطين. لنأخذ تطبيق ويب Spring MVC أحادي الطبقة كمثال:

إذا قمنا بتنفيذ الطبقة [Web] باستخدام Spring MVC، فسنحصل على بنية ويب MVC ولكن ليس بنية متعددة الطبقات. هنا، ستتولى الطبقة [web] كل شيء: العرض، والوظيفة، والوصول إلى البيانات. هذه هي الإجراءات التي ستقوم بهذا العمل.

الآن، لننظر إلى بنية ويب متعددة الطبقات:

يمكن تنفيذ الطبقة [Web] بدون إطار عمل وبدون اتباع النموذج MVC. لدينا إذن بنية متعددة الطبقات، لكن الطبقة الويب لا تنفذ النموذج MVC.

على سبيل المثال، في عالم .NET، يمكن تنفيذ الطبقة [Web] المذكورة أعلاهأعلاه يمكن تنفيذها باستخدام ASP.NET و MVC، وبذلك نحصل على بنية طبقات تحتوي على طبقة [Web] من النوع MVC. وبعد ذلك، يمكن استبدال هذه الطبقة ASP.NET MVC بطبقة ASP.NET تقليدية (WebForms) مع الحفاظ على بقية (المهنة، DAO، ORM) كما هو. نحصل عندئذٍ على بنية طبقات تحتوي على طبقة [Web] التي لم تعد من النوع MVC.

في MVC، ذكرنا أن النموذج M هو نموذج العرض V، c.a.d. مجموعة البيانات المعروضة بواسطة العرض V. يتم تقديم تعريف آخر للنموذج M لـ MVC:

يعتبر العديد من المؤلفين أن ما يقع على يمين الطبقة [Web] يشكل النموذج M لـ MVC. لتجنب الغموض، يمكننا التحدث عن:

  • نموذج المجال عند الإشارة إلى كل ما يقع على يمين الطبقة [Web]
  • نموذج العرض عند الإشارة إلى البيانات المعروضة بواسطة عرض V

فيما يلي، سيشير مصطلح "نموذج M" حصريًا إلى نموذج عرض V.

13.4. مشروع ويب / jSON مع Spring MVC

يقدم موقع [http://spring.io/guides] دروسًا تعليمية للمبتدئين لاكتشاف نظام Spring. سنتبع إحدى هذه الدروس لاكتشاف تكوين Maven اللازم لمشروع Spring MVC.

13.4.1. مشروع العرض التوضيحي

  • في [1]، نقوم باستيراد أحد أدلة Spring؛
  • في [2]، نختار المثال [Rest Service
  • في [3]، نختار مشروع Maven؛
  • في [4]، نأخذ النسخة النهائية من الدليل؛
  • في [5]، نقوم بالتحقق؛
  • في [6]، المشروع المستورد؛

غالبًا ما يُطلق على الخدمات الويب التي يمكن الوصول إليها عبر معايير URL والتي تقدم نصًا jSON اسم خدمات REST (REpresentational State Transfer). يُطلق على الخدمة اسم Restful إذا كانت تتوافق مع قواعد معينة.

لنلقِ نظرة الآن على المشروع المستورد، بدءًا من تكوين Maven الخاص به.

13.4.2. تكوين Maven

ملف [pom.xml] هو التالي:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
</project>
  • الأسطر 6-8: خصائص مشروع Maven. ينقص علامة [<packaging>] التي تشير إلى نوع الملف الناتج عن تجميع Maven. في حالة عدم وجودها، يتم استخدام النوع [jar]. وبالتالي، فإن التطبيق هو تطبيق قابل للتنفيذ من نوع وحدة التحكم، وليس تطبيق ويب حيث يكون التعبئة [war]؛
  • الأسطر 10-14: يحتوي مشروع Maven على مشروع أبوي [spring-boot-starter-parent]. وهو الذي يحدد معظم تبعيات المشروع. قد تكون هذه التبعيات كافية، وفي هذه الحالة لا نضيف المزيد، أو قد لا تكون كافية، وفي هذه الحالة نضيف التبعيات الناقصة؛
  • الأسطر 17-20: يجلب الأرتيفاكت [spring-boot-starter-web] معه المكتبات اللازمة لمشروع Spring MVC من نوع خدمة الويب حيث لا توجد عروض مُنشأة. يحتوي هذا الأرتيفاكت على عدد كبير جدًا من المكتبات، بما في ذلك مكتبات خادم Tomcat المدمج. سيتم تشغيل التطبيق على هذا الخادم؛

المكتبات التي توفرها هذه التهيئة كثيرة جدًا:

فيما يلي نرى الأرشيفات الثلاثة لخادم Tomcat.

13.4.3. بنية خدمة Spring [web / jSON]

بالنسبة لخدمة الويب / jSON، يقوم Spring MVC بتنفيذ نموذج MVC بالطريقة التالية:

  • في [4a]، يتم تحويل النموذج الذي هو فئة Java إلى سلسلة jSON بواسطة مكتبة jSON؛
  • إلى [4b]، يتم إرسال هذه السلسلة jSON إلى المتصفح؛

13.4.4. وحدة التحكم C

  

يحتوي التطبيق المستورد على وحدة التحكم التالية:


package hello;

import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}
  • السطر 9: التعليق التوضيحي [@RestController] يجعل من الفئة [GreetingController] وحدة تحكم Spring، أي أن أساليبها مسجلة لمعالجة URL. لقد رأينا التعليق التوضيحي المماثل [@Controller]. كانت نتيجة أساليب هذا المتحكم هي نوع [String] الذي كان اسم العرض المراد عرضه. الأمر مختلف هنا. تُرجع أساليب وحدة التحكم من النوع [@RestController] كائنات يتم تسلسلها لإرسالها إلى المتصفح. يعتمد نوع التسلسل الذي يتم إجراؤه على تكوين Spring MVC. هنا، سيتم تسلسلها إلى jSON. إن وجود مكتبة jSON في تبعيات المشروع هو ما يجعل Spring Boot يقوم، عن طريق التكوين التلقائي، بتكوين المشروع بهذه الطريقة؛
  • السطر 14: تشير التعليقة التوضيحية [@RequestMapping] إلى URL الذي تعالجه الطريقة، وهنا URL [/greeting
  • السطر 15: سبق أن شرحنا التعليق التوضيحي [@RequestParam]. والنتيجة التي تُرجعها الطريقة هي كائن من النوع [Greeting].
  • السطر 12: عدد صحيح طويل من النوع الذري. وهذا يعني أنه يدعم التنافس في الوصول. قد ترغب عدة خيوط في زيادة المتغير [counter] في نفس الوقت. وسيتم ذلك بشكل صحيح. لا يمكن لخيط قراءة قيمة العداد إلا إذا كان الخيط الذي يقوم بتعديله قد انتهى من تعديله.

13.4.5. النموذج M

النموذج M الناتج عن الطريقة السابقة هو الكائن [Greeting] التالي:

  

package hello;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

ستنشئ عملية التحويل jSON لهذا الكائن سلسلة الأحرف {"id":n,"content":"نص"}. في النهاية، ستكون سلسلة jSON الناتجة عن طريقة وحدة التحكم بالشكل التالي:

{"id":2,"content":"Hello, World!"}

أو

{"id":2,"content":"Hello, John!"}

13.4.6. التنفيذ

  

الفئة [Application.java] هي الفئة القابلة للتنفيذ في المشروع. ورمزها هو كما يلي:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

لقد سبق أن تناولنا هذا الرمز وشرحناه في المثال السابق.

13.4.7. تنفيذ المشروع

لنقم بتنفيذ المشروع:

 

نحصل على سجلات وحدة التحكم التالية:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.9.RELEASE)

2014-11-28 15:22:55.005  INFO 3152 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046  INFO 3152 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762  INFO 3152 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567  INFO 3152 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738  INFO 3152 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-11-28 15:22:56.740  INFO 3152 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869  INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870  INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478  INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481  INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] إلى معالج من النوع [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] معالج من النوع [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231  INFO 3152 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318  INFO 3152 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319  INFO 3152 --- [           main] hello.Application                        : Started Application in 3.788 seconds (JVM running for 4.424)
  • السطر 13: يبدأ تشغيل خادم Tomcat على المنفذ 8080 (السطر 12)؛
  • السطر 17: السيرفلت [DispatcherServlet] موجود؛
  • السطر 20: تم اكتشاف الطريقة [GreetingController.greeting

لاختبار تطبيق الويب، نطلب URL [http://localhost:8080/greeting]:

 

نتلقى بالفعل السلسلة المتوقعة jSON. قد يكون من المثير للاهتمام رؤية الرؤوس HTTP المرسلة من الخادم. للقيام بذلك، سنستخدم ملحق Chrome المسمى [Advanced Rest Client] (Chrome / Ctrl-T / قائمة [Applications] / [Advanced Rest Client] - انظر الملحقات الفقرة 22.5):

  • في [1]، URL المطلوب؛
  • في [2]، تُستخدم الطريقة GET؛
  • في [3]، الرد jSON؛
  • في [4]، أشار الخادم إلى أنه يرسل استجابة بتنسيق jSON؛
  • في [5]، يُطلب نفس URL ولكن هذه المرة مع POST؛
  • في [7]، يتم إرسال المعلومات إلى الخادم في شكل [urlencoded
  • في [6]، المعلمة name مع قيمتها؛
  • في [8]، يُعلم المتصفح الخادم بأنه يرسل إليه معلومات [urlencoded
  • في [9]، رد الخادم jSON؛

13.4.8. إنشاء أرشيف قابل للتنفيذ

نقوم الآن بإنشاء أرشيف قابل للتنفيذ:

  • في [1]: يتم تنفيذ هدف Maven؛
  • في [2]: هناك هدفان (goals): [clean] لحذف المجلد [target] من مشروع Maven، و[package] لإعادة إنشائه؛
  • في [3]: سيتم إنشاء المجلد [target] في هذا المجلد؛
  • في [4]: يتم إنشاء الهدف؛

في السجلات التي تظهر في وحدة التحكم، من المهم أن يظهر المكون الإضافي [spring-boot-maven-plugin]. فهو الذي يقوم بإنشاء الأرشيف القابل للتنفيذ.

[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

باستخدام وحدة التحكم، ننتقل إلى المجلد الذي تم إنشاؤه:


D:\Temp\wksSTS\gs-rest-service\target>dir
 ...
11/06/2014  15:30    <DIR>          classes
11/06/2014  15:30    <DIR>          generated-sources
11/06/2014  15:30        11 073 572 gs-rest-service-0.1.0.jar
11/06/2014  15:30             3 690 gs-rest-service-0.1.0.jar.original
11/06/2014  15:30    <DIR>          maven-archiver
11/06/2014  15:30    <DIR>          maven-status
...
  • السطر 5: الأرشيف الذي تم إنشاؤه؛

يتم تشغيل هذا الملف المضغوط بالطريقة التالية:


D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)

2014-06-11 15:32:47.088  INFO 4972 --- [           main] hello.Application
                  : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...

الآن بعد تشغيل تطبيق الويب، يمكننا الوصول إليه باستخدام متصفح:

 

13.4.9. نشر التطبيق على خادم Tomcat

كما تم في المشروع السابق، نقوم بتعديل الملف [pom.xml] بالطريقة التالية:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>

    ...
</project>
  • السطر 9: يجب الإشارة إلى أننا سنقوم بإنشاء أرشيف war (Web ARchive

كما يجب تكوين تطبيق الويب. في حالة عدم وجود ملف [web.xml]، يتم ذلك باستخدام فئة موروثة من [SpringBootServletInitializer]:

  

الفئة [ApplicationInitializer] هي كما يلي:


package hello;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class ApplicationInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

}
  • السطر 6: الفئة [ApplicationInitializer] توسع الفئة [SpringBootServletInitializer
  • السطر 9: يتم إعادة تعريف الطريقة [configure] (السطر 8)؛
  • السطر 10: يتم تحديد الفئة التي تهيئ المشروع؛

لتنفيذ المشروع، يمكن اتباع الخطوات التالية:

  • في [1-2]، يتم تنفيذ المشروع على أحد الخوادم المسجلة في IDE Eclipse؛

بعد ذلك، يمكن طلب URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] في متصفح:

 

13.4.10. الخلاصة

لقد قدمنا نوعًا من مشاريع Spring MVC حيث ترسل تطبيق الويب دفقًا jSON إلى المتصفح. سنقوم الآن بتطوير تطبيق ويب / jSON لعرض قاعدة البيانات [dbintrospringdata] التي تمت دراستها في البرنامج التعليمي [Introduction à Spring Data] على الويب.

13.5. عرض قاعدة البيانات [dbintrospringdata] على الويب

13.5.1. بنية خدمة الويب / jSON

سنقوم بتنفيذ البنية التالية:

يتم تنفيذ الطبقتين [DAO] و [JPA] بواسطة التطبيق المكتوب في البرنامج التعليمي [Introduction à Spring Data].

13.5.2. تثبيت قاعدة البيانات

  

يتيح البرنامج النصي SQL [dbintrospringdata.sql] إنشاء قاعدة البيانات MySQL اللازمة للاختبارات.

13.5.3. مشروع Eclipse لخدمة الويب / jSON

مشروع Eclipse لخدمة الويب / jSON هو التالي:

  

إنه مشروع Maven يحتوي على الملف [pom.xml] التالي:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.webjson</groupId>
    <artifactId>intro-server-webjson01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>intro-server-webjson01</name>
    <description>démo spring mvc</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>istia.st.springdata</groupId>
            <artifactId>intro-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • الأسطر 11-15: مشروع Maven الأصلي المستخدم بالفعل للطبقة [DAO
  • الأسطر 18-22: التبعية لطبقة [DAO
  • الأسطر 23-26: التبعية على الأرتيفاكت [spring-boot-starter-web]. يجلب هذا الأرتيفاكت معه جميع التبعيات اللازمة لإنشاء خدمة ويب / jSON. كما يجلب مكتبات غير ضرورية. لذلك، سيكون من الضروري إجراء تكوين أكثر دقة. لكن هذا التكوين مفيد للبدء؛
  • السطور 28-30: التبعية على الأداة [spring-boot-starter] تسمح بإدارة تعليقات Spring Boot؛

التبعيات التي يجلبها هذا التكوين هي التالية:

  • في [1]، نرى أن Eclipse قد رصد التبعية على أرشيف مشروع [intro-spring-data-01

التبعيات المذكورة أعلاه هي تبعيات كل من الطبقة [DAO] والطبقة [web].

13.5.3.1. تكوين الطبقة [web]

يتم تكوين الطبقة [web] بواسطة ملف [AppConfig]:

  

تقوم الفئة [WebConfig] بتكوين الطبقة [web]:


package spring.webjson.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // -------------------------------- تكوين الطبقة [web]
    @Autowired
    private ApplicationContext context;

    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }

    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }

    // مرشحات jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // أداة التعيين jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // النتيجة
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // مُخطِط jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // النتيجة
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // مُخطِط jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // النتيجة
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // مُخطِط jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // النتيجة
        return mapper;
    }
}
  • السطر 18: التوضيح [@EnableWebMvc] يؤدي إلى تكوينات تلقائية لإطار عمل Spring MVC؛
  • السطر 19: الفئة [WebConfig] توسع فئة Spring [WebMvcConfigurerAdapter] لإعادة تعريف بعض الفاصوليا (السطور 26-40)؛
  • السطران 22-23: حقن سياق Spring؛
  • الأسطر 25-29: تعريف سيرفلت إطار عمل Spring MVC، وهو الذي يوجه الطلبات HTTP إلى وحدة التحكم والطريقة الصحيحة. [DispatcherServlet] هي فئة من Spring؛
  • الأسطر 31-34: يُشار إلى أن هذه الخدمة تعالج جميع URL؛
  • الأسطر 36-39: إن وجود هذا البين هو الذي سيقوم بتنشيط خادم Tomcat الموجود في أرشيفات المشروع. وسوف ينتظر الطلبات على المنفذ 8080؛
  • الأسطر 42-91: حبات ستُستخدم لإدارة مرشحات jSON؛
  • الأسطر 42-45: مخطط jSON بدون مرشحات؛
  • الأسطر 47-57: أداة التعيين jSON التي تتيح الحصول على فئة مع منتجاتها. تجدر الإشارة إلى أنه عند طلب فئة مع منتجاتها، يجب تكوين كل من مرشح jSON الخاص بالفئة [Categorie] ومرشح الفئة [Produit]. وهذا هو الحال دائمًا. عند تسلسل/إلغاء تسلسل فئة في jSON، يجب تكوين مرشح jSON للفئة ومرشحات جميع التبعيات المراد تضمينها فيها؛
  • الأسطر 59-69: المُعين jSON الذي يسمح بالحصول على منتج مع فئته؛
  • الأسطر 71-80: المُعَيِّن jSON الذي يسمح بالحصول على فئة بدون منتجاتها؛
  • الأسطر 82-91: المُخطِط jSON الذي يسمح بالحصول على منتج بدون فئته؛

تقوم الفئة [AppConfig] بتكوين التطبيق بأكمله، أي الطبقات [web] و [DAO]:


package spring.webjson.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import spring.data.config.DaoConfig;

@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {

}
  • السطر 9: يتم استيراد حبوب الطبقة [DAO] وحبوب الطبقة [web]؛
  • السطر 8: يشير إلى الحزم التي يمكن العثور فيها على حبات Spring أخرى؛

تجدر الإشارة إلى أننا لم نستخدم التعليق التوضيحي [@EnableAutoConfiguration] في أي مكان. لقد فضلنا التحكم في التكوين بأنفسنا.

13.5.4. نموذج التطبيق

  

الفئة [ApplicationModel] هي كما يلي:


package spring.webjson.models;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

@Component
public class ApplicationModel implements IDao {

    // الطبقة [DAO]
    @Autowired
    private IDao dao;

    @Override
    public void addProduits(List<Produit> produits) {
        dao.addProduits(produits);
    }

    @Override
    public void deleteAllProduits() {
        dao.deleteAllProduits();
    }

    @Override
    public void updateProduits(List<Produit> produits) {
        dao.updateProduits(produits);
    }

    @Override
    public List<Produit> getAllProduits() {
        return dao.getAllProduits();
    }

    @Override
    public void addCategories(List<Categorie> categories) {
        dao.addCategories(categories);
    }

    @Override
    public void deleteAllCategories() {
        dao.deleteAllCategories();
    }

    @Override
    public void updateCategories(List<Categorie> categories) {
        dao.updateCategories(categories);
    }

    @Override
    public List<Categorie> getAllCategories() {
        return dao.getAllCategories();
    }

    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        return dao.getProduitByIdWithCategorie(idProduit);
    }

    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        return dao.getProduitByNameWithCategorie(nom);
    }

    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        return dao.getCategorieByIdWithProduits(idCategorie);
    }

    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        return dao.getCategorieByNameWithProduits(nom);
    }

    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        return dao.getProduitByIdWithoutCategorie(idProduit);
    }

    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        return dao.getCategorieByIdWithoutProduits(idCategorie);
    }

    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {        
        return dao.getProduitByNameWithoutCategorie(nom);
    }

    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        return dao.getCategorieByNameWithoutProduits(nom);
    }

}
  • السطر 12: الفئة هي فئة Spring فردية؛
  • السطر 13: التي تنفذ واجهة [IDao] من الطبقة [DAO
  • السطران 16-17: حقن مرجع في الطبقة [DAO
  • السطور 19-99: تنفيذ واجهة [IDao

تتطور بنية طبقة الويب على النحو التالي:

  • في [2b]، تتواصل أساليب وحدة التحكم أو وحدات التحكم مع العنصر الفردي [ApplicationModel

توفر هذه الاستراتيجية مرونة في إدارة ذاكرة التخزين المؤقت المحتملة. يمكن استخدام الفئة [ApplicationModel] لتخزين المعلومات التي يتم الحصول عليها من الطبقة [DAO] أو بيانات التكوين. قد يكون ذلك مفيدًا عندما لا يكون لدينا سيطرة على الطبقة [DAO]. قد تتطور استراتيجية التخزين المؤقت هذه بمرور الوقت. لن تؤثر التعديلات على كود وحدة التحكم أو وحدات التحكم.

13.5.5. وحدة التحكم

  

لدينا هنا وحدة تحكم واحدة فقط، وهي الفئة [MyController].

13.5.5.1. URL المعروضة

فئات URL المعروضة بواسطة وحدة التحكم هذه هي التالية:


    @RequestMapping(value = "/addProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addProduits(HttpServletRequest request) {
...
    }
يضيف منتجات إلى قاعدة البيانات. يتم نشرها. الرد هو السلسلة jSON وهي قائمة بالمنتجات المضافة مع مفتاحها الأساسي.

    @RequestMapping(value = "/deleteAllProduits",
method = RequestMethod.GET)
    public String deleteAllProduits() {
..
    }
حذف جميع المنتجات من قاعدة البيانات.

    @RequestMapping(value = "/updateProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String updateProduits(HttpServletRequest request) {
..
    }

يتم تحديث المنتجات في قاعدة البيانات. يتم نشرها. الرد هو سلسلة jSON من قائمة المنتجات المحدثة.

    @RequestMapping(value = "/getAllProduits",
method = RequestMethod.GET)
    public String getAllProduits() {
..
    }

يحصل على السلسلة jSON من جميع المنتجات.

    @RequestMapping(value = "/addCategories",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addCategories(HttpServletRequest request) {
..
    }

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

    @RequestMapping(value = "/deleteAllCategories",
method = RequestMethod.GET)
    public String deleteAllCategories() {
...
    }

يحذف جميع الفئات من قاعدة البيانات بالإضافة إلى جميع المنتجات الموجودة فيها. بعد ذلك تصبح قاعدة البيانات فارغة.

    @RequestMapping(value = "/updateCategories",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String updateCategories
(HttpServletRequest request) {
...
    }

يُحدّث الفئات في قاعدة البيانات. يتم نشر هذه الفئات. والرد هو قائمة بالفئات التي تم تحديثها. إذا كانت الفئات تحتوي على منتجات، يتم تحديث هذه المنتجات أيضًا في قاعدة البيانات. يُرجع السلسلة jSON للفئات التي تم تعديلها؛

    @RequestMapping(value = "/getAllCategories",
method = RequestMethod.GET)
    public String getAllCategories() {
...
    }

يحصل على السلسلة jSON من جميع الفئات.

    @RequestMapping(value = "/getProduitByIdWithCategorie/{idProduit}",
method = RequestMethod.GET)
    public String getProduitByIdWithCategorie
(@PathVariable("idProduit") Long idProduit) {
...
    }

يحصل على السلسلة jSON لمنتج محدد بواسطة معرّفه، مع فئته.

    @RequestMapping(value = "/getProduitByIdWithoutCategorie/{idProduit}",
method = RequestMethod.GET)
    public String getProduitByIdWithoutCategorie
@PathVariable("idProduit") Long idProduit) {
...
    }

يحصل على السلسلة jSON لمنتج محدد بواسطة معرّفه، بدون فئته.

    @RequestMapping(value = "/getProduitByNameWithCategorie/{nom}",
method = RequestMethod.GET)
    public String getProduitByNameWithCategorie(
@PathVariable("nom") String nom) {
...
    }

يحصل على السلسلة jSON لمنتج محدد باسمه، مع فئته.

    @RequestMapping(value = "/getProduitByNameWithoutCategorie/{nom}",
method = RequestMethod.GET)
    public String getProduitByNameWithoutCategorie
(@PathVariable("nom") String nom) {
...
    }

يحصل على السلسلة jSON لمنتج محدد باسمه، بدون فئته.

    @RequestMapping(value = "/getCategorieByIdWithProduits/{idCategorie}",
 method = RequestMethod.GET)
    public String getCategorieByIdWithProduits
@PathVariable("idCategorie") Long idCategorie) {
...
    }

يحصل على السلسلة jSON من فئة محددة بواسطة معرفها، مع منتجاتها.

    @RequestMapping(value = "/getCategorieByNameWithProduits/{nom}",
method = RequestMethod.GET)
    public String getCategorieByNameWithProduits
(@PathVariable("nom") String nom) {
...
    }

يحصل على السلسلة jSON من فئة محددة باسمها، مع منتجاتها.

    @RequestMapping(value = "/getCategorieByNameWithoutProduits/{nom}",
method = RequestMethod.GET)
    public String getCategorieByNameWithoutProduits(
@PathVariable("nom") String nom) {
...
    }

يحصل على السلسلة jSON من فئة محددة باسمها، بدون منتجاتها.

    @RequestMapping(value = "/getCategorieByIdWithoutProduits/{idCategorie}",
method = RequestMethod.GET)
    public String getCategorieByIdWithoutProduits(
@PathVariable("idCategorie") Long idCategorie) {
...
    }

يحصل على السلسلة jSON من فئة محددة بواسطة معرفها بدون منتجاتها.

تتوافق URL المعروضة مع طرق واجهة [IDao] للطبقة [DAO]. طرق خدمة الويب / jSON مبنية جميعها على نفس النموذج. سنقوم بفحص بعضها.

13.5.5.2. هيكل وحدة التحكم

هيكل وحدة التحكم هو كما يلي:


package spring.webjson.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;

import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;

@Controller
public class MyController {

    // تبعيات Spring
    @Autowired
    private ApplicationModel application;

    // مرشحات jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;

    // الطبقة [MyController] هي فئة فردية ولا يتم إنشاء مثيل لها إلا مرة واحدة
    
    public MyController() {
        // System.out.println("MyController");
    }

    @RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addProduits(HttpServletRequest request) throws JsonProcessingException {
        ...
    }

  • السطر 28: التعليق التوضيحي [@Controller] يجعل الفئة مكونًا في Spring؛
  • السطران 32-33: حقن مرجع في الفئة [ApplicationModel
  • الأسطر 36-50: حقن مراجع في المخططين jSON؛
  • السطر 58: URL المعروضة هي [/addProduits]. يجب على العميل استخدام طريقة [POST] لتقديم طلبه (method = RequestMethod.POST). يجب عليه إرسال القيمة المرسلة في شكل سلسلة jSON (consumes = "application/json; charset=UTF-8"). تقوم الطريقة نفسها بإرجاع الرد إلى العميل (السطر 59). سيكون ذلك سلسلة أحرف (السطر 60). سيتم إرسال الرأس HTTP [Content-type : application/json; charset=UTF-8] إلى العميل لإعلامه بأنه سيتلقى سلسلة jSON (السطر 58)؛
  • السطر 60: تعرض الطريقة [addProduits] السلسلة jSON من قائمة المنتجات المضافة إلى قاعدة البيانات؛

13.5.5.3. استجابة طرق وحدة التحكم

تُرجع جميع طرق وحدة التحكم استجابة من النوع [Response] كما يلي:

  

package spring.webjson.service;

import java.util.List;

public class Response<T> {

    // ----------------- الخصائص
    // حالة العملية
    private int status;
    // رسائل الخطأ المحتملة
    private List<String> messages;
    // نص الرد
    private T body;

    // المنشئات
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // مُستردات ومُعيّنات
    ...
}
  • السطر 5: الاستجابة تغلف نوع T؛
  • السطر 13: الاستجابة من النوع T؛
  • الأسطر 9-11: من الممكن أن تواجه إحدى الطرق استثناءً. في هذه الحالة، ستُرجع استجابةً تحتوي على:
    • السطر 9: status!=0؛
    • السطر 11: قائمة الأخطاء التي تمت مواجهتها؛

13.5.5.4. L'URL [/addProduits]

L'URL [/addProduits] تتم معالجتها بواسطة الطريقة التالية:


@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addProduits(HttpServletRequest request) throws JsonProcessingException {
        // الرد
        Response<List<Produit>> response;
        try {
            // استرداد القيمة المرسلة
            String body = CharStreams.toString(request.getReader());
            List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
            });
            // إعادة إنشاء الارتباط بين المنتجات والفئات
            for (Produit produit : produits) {
                produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
            }
            // يتم حفظ المنتجات
            application.addProduits(produits);
            response = new Respon    se<List<Produit>>(0, null, produits);
        } catch (DaoException e1) {
            response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
        } catch (Exception e2) {
            response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
        }
        // الرد jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
    }
  • السطر 3: تقبل الطريقة كمعلمة [HttpServletRequest request] التي تحتوي على جميع المعلومات المتعلقة بطلب العميل؛
  • السطر 5: الرد الذي سيتم إرساله إلى العميل: قائمة بالمنتجات؛
  • السطر 8: يتم استرداد القيمة المرسلة. تنتمي الفئة [CharStreams] إلى المكتبة [Google Guava] التي تمت إضافة مرجعها في الملف [pom.xml]. نحصل على السلسلة jSON التي أرسلها العميل. يجب إزالة التسلسل منها لاستخدامها؛
  • الأسطر 8-10: تتم عملية إزالة التسلسل. نحصل على قائمة بالمنتجات حيث يحتوي كل منتج على حقل [categorie=null
  • الأسطر 12-14: يتم إعادة تعيين الحقل [categorie] لجميع المنتجات في القائمة. وللقيام بذلك، يتم استخدام الحقل [idCategorie] للمنتج الذي تم تهيئته؛
  • السطر 16: يتم إدراج المنتجات في قاعدة البيانات؛
  • السطر 17: يتم تهيئة الكائن [response] بقائمة المنتجات؛
  • السطران 18-19: الحالة التي تواجه فيها الطريقة استثناءً من الطبقة [DAO]. يتم تهيئة الاستجابة بـ [status=1000] (رمز الخطأ) [messages=e1.getMessages()]، أي يتم إرسال قائمة الأخطاء التي واجهت الجانب الخادم إلى العميل؛
  • السطران 20-21: الحالة التي تواجه فيها الطريقة نوعًا آخر من الاستثناءات. يتم تهيئة الاستجابة بـ [status=1000] (رمز الخطأ) [messages=getErreursForException(e)] حيث [getErreursForException] هي طريقة خاصة بالفئة تعرض قائمة الأخطاء المرتبطة باستثناءات مكدس الاستثناءات لـ e، و [body=null
  • السطر 24: يتم إرجاع السلسلة jSON من الاستجابة؛

13.5.5.5. L'URL [/getAllProduits]

يتم معالجة URL [/getAllProduits] بالطريقة التالية:


    @RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits() throws JsonProcessingException {
        // الرد
        Response<List<Produit>> response;
        try {
            response = new Response<List<Produit>>(0, null, application.getAllProduits());
        } catch (DaoException e1) {
            response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
        } catch (Exception e2) {
            response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
        }
        // الرد jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
  • السطر 1: يتم طلب URL [/getAllProduits] باستخدام عملية [GET]. وتنتج jSON؛
  • السطر 2: ترسل الطريقة بنفسها الرد jSON إلى العميل؛
  • السطر 5: ترسل الطريقة السلسلة jSON من النوع [Response<List<Produit>>]؛
  • السطر 7: يتم طلب المنتجات بدون فئتها؛
  • الأسطر 8-12: في حالة حدوث خطأ، يتم تهيئة الرد برمز ورسائل خطأ؛
  • السطر 14: يتم إرسال الرد jSON إلى العميل؛

13.5.5.6. Conclusion

لن نعرض الطرق الأخرى للمتحكم. فهي تشبه إحدى الطريقتين اللتين عرضناهما للتو.

13.5.6. فئة تنفيذ خدمة الويب / jSON

  

فئة [Boot] هي فئة التنفيذ للمشروع:


package spring.webjson.boot;

import org.springframework.boot.SpringApplication;

import spring.webjson.server.config.AppConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}
  • السطر 10: يتم تنفيذ الطريقة الثابتة [SpringApplication.run]. فئة [SpringApplication] هي فئة من مشروع [Spring Boot] (السطر 3). يتم تمرير معلمتين إليها:
    • [AppConfig.class]: الفئة التي تهيئ التطبيق بأكمله؛
    • [args]: أي معلمات يتم تمريرها إلى الطريقة [main] في السطر 9. لا يتم استخدام هذه المعلمة هنا؛

عند تنفيذ هذه الفئة، نحصل على السجلات التالية:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-24 16:22:46.608  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Starting Boot on Gportpers3 with PID 9492 (D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02\target\classes started by ST in D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02)
2015-03-24 16:22:46.654  INFO 9492 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:47.521  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2015-03-24 16:22:47.569  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'entityManagerFactory': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=spring.data.config.DaoConfig; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class spring.data.config.DaoConfig] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=true; factoryBeanName=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]]
2015-03-24 16:22:48.137  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$405db6ba] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.162  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.172  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.178  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.586  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2015-03-24 16:22:48.850  INFO 9492 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2015-03-24 16:22:48.852  INFO 9492 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.20
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2342 ms
2015-03-24 16:22:49.645  INFO 9492 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2015-03-24 16:22:49.650  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'characterEncodingFilter' to: [/*]
2015-03-24 16:22:49.651  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015-03-24 16:22:50.380  INFO 9492 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-24 16:22:50.392  INFO 9492 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-24 16:22:50.478  INFO 9492 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-24 16:22:50.480  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-24 16:22:50.483  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-24 16:22:50.697  INFO 9492 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-24 16:22:50.806  INFO 9492 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2015-03-24 16:22:51.058  INFO 9492 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-24 16:22:52.581  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:52.654  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.addProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.updateProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.getAllProduits()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.getAllCategories()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.addCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.updateCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithoutProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithoutProduits(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithoutCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithoutCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithCategorie(java.lang.Long)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithProduits(java.lang.String)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllCategories()
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithoutProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithoutProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllProduits()
2015-03-24 16:22:52.658  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithoutCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithoutCategorie(java.lang.Long)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.691  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] إلى معالج من النوع [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.692  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] إلى معالج من النوع [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.742  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:53.001  INFO 9492 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2015-03-24 16:22:53.106  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-24 16:22:53.108  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Started Boot in 6.752 seconds (JVM running for 7.433)
  • الأسطر 17-19: بدء تشغيل خادم Tomcat الذي سيقوم بتنفيذ خدمة الويب / jSON؛
  • الأسطر 25-33: إنشاء الطبقة [DAO
  • الأسطر 32-51: اكتشاف URL المعرضة؛

13.5.7. اختبارات خدمة الويب / jSON

لإجراء الاختبارات، نقوم بإنشاء قاعدة البيانات MySQL [dbintrospringdata] من البرنامج النصي SQL [dbintrospringdata.sql]:

  

وبعد ذلك، نستخدم العميل [Advanced Rest Client] (انظر الفقرة 22.5) لاستعلام URL المعروضة بواسطة خدمة الويب / jSON (يجب تشغيل خدمة الويب / jSON).

  • في [1-3]، نطلب URL [/getAllCategories] عبر أمر HTTP GET؛

نحصل على الرد التالي:

  • في [1]، طلب HTTP من العميل؛
  • في [2]، استجابة HTTP من الخادم؛
  • إلى [3]، ويشير الحالة [200 OK] إلى أن الخادم قد عالج الطلب بشكل صحيح؛
  • في [4]، استجابة الخادم jSON؛

الرد الكامل jSON هو كما يلي:


{"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
  • status:0 يعني أنه لم تحدث أخطاء من جانب الخادم؛
  • messages: null تعني أنه لا توجد رسائل خطأ؛
  • body: هو نص الرد، وهنا قائمة الفئات مع منتجاتها. هناك فئتان تحتوي كل منهما على 5 منتجات؛

سنقوم بإضافة المنتج [produit15] إلى الفئة [categorie1]. وللقيام بذلك، سنستخدم URL [/addCategories] الذي يحمل الرمز التالي:


@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addCategories(HttpServletRequest request) throws JsonProcessingException {
        Response<List<Categorie>> response;
        ObjectMapper mapper = context.getBean(ObjectMapper.class);
        // يتم الاحتفاظ بالفئات
        try {
            // استرداد القيمة المنشورة
            String body = CharStreams.toString(request.getReader());
            mapper.setFilters(jsonFilterCategorieWithProduits);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // إعادة إنشاء الارتباط بين المنتجات والفئات
            for (Categorie categorie : categories) {
                Set<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    for (Produit produit : categorie.getProduits()) {
                        produit.setCategorie(categorie);
                    }
                }
            }
            // يتم الاحتفاظ بالفئات
            application.addCategories(categories);
            response = new Response<List<Categorie>>(0, null, categories);
        } catch (Exception e) {
            response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
        }
        // الرد jSON
        return mapper.writeValueAsString(response);
    }
  • السطر 1: يجب على العميل إنشاء POST ويجب أن تكون القيمة المنشورة سلسلة jSON؛
  • الأسطر 9-12: يجب أن تكون القيمة المنشورة قائمة بالفئات مع المنتجات المرتبطة بها؛

سنقوم بإنشاء فئة [categorie2] مع منتج [produit21]. وبالتالي، فإن السلسلة jSON المراد إرسالها هي التالية:

[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]

يتم إجراء الطلب إلى خدمة الويب / jSON بالطريقة التالية:

  • في [1]، URL المطلوب؛
  • في [2]، يتم طلبها عبر عملية POST؛
  • في [3]، تم إرسال السلسلة jSON؛
  • في [4]، يتم إخطار الخادم بأنه سيتم إرسال jSON إليه؛

رد الخادم هو التالي:

  • في [1]، نرى أن الفئة والمنتج لديهما الآن مفتاح أساسي، مما يدل على أنهما قد تم إدخالهما على الأرجح في قاعدة البيانات. سنقوم بالتحقق من ذلك باستخدام URL [/getCategorieByNameWithProduits/categorie2]:

نحصل على النتيجة التالية:

لقد حصلنا بالفعل على الفئة [categorie2] مع منتجها الوحيد [produit21]. يمكننا أيضًا طلب المنتج فقط. لنستخدم لذلك URL [/getProduitByIdWithoutCategorie/1859]:

نحصل على النتيجة التالية:

يمكن إجراء جميع العمليات [GET] في متصفح بسيط:

 

ندعو القارئ إلى تجربة عمليات URL الأخرى الخاصة بخدمة الويب / json.

13.6. عميل مبرمج لخدمة الويب / jSON

الآن بعد أن أصبحت قاعدة [dbintrospringdata] متاحة على الويب، سنقوم بكتابة تطبيق يستفيد منها. سيكون لدينا عندئذٍ بنية العميل / الخادم التالية:

سيكون للتطبيق العميل طبقتان:

  • طبقة [DAO] [2] للتواصل مع تطبيق الويب / jSON الذي يعرض قاعدة البيانات؛
  • طبقة اختبار JUnit [1] للتحقق من أن العميل والخادم يؤديان عملهما بشكل صحيح؛

13.6.1. مشروع Eclipse

مشروع Eclipse الخاص بالعميل هو التالي:

  
  • المجلد [src/main/java] ينفذ الطبقة [DAO
  • المجلد [src/test/java] ينفذ الاختبارات JUnit؛

13.6.2. تكوين Maven للمشروع

المشروع هو مشروع Maven تم تكوينه بواسطة الملف [pom.xml] التالي:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.webjson</groupId>
    <artifactId>intro-client-webjson-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <description>Client console du serveur web / jSON</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <!-- مكتبة jSON المستخدمة بواسطة Spring -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- مكون يستخدمه Spring RestTemplate -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
            <scope>test</scope>
        </dependency>
        <!-- مكتبة السجلات -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- اختبار Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- المكونات الإضافية -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

    <name>intro-client-webjson-01</name>
</project>
  • الأسطر 14-18: مشروع Maven الأصلي [spring-boot-starter-parent] الذي يسمح لنا بتحديد عدد من التبعيات دون تحديد إصداراتها، حيث يتم تحديد الإصدارات في المشروع الأصلي؛
  • الأسطر 22-25: على الرغم من أننا لا نكتب تطبيق ويب، إلا أننا نحتاج إلى التبعية [spring-web] التي تجلب معها الفئة [RestTemplate] التي تسمح بالتفاعل بسهولة مع تطبيق ويب / jSON؛
  • الأسطر 27-34: مكتبة jSON؛
  • الأسطر 36-39: تبعية ستسمح لنا بتعيين timeout لطلبات HTTP من العميل. timeout هو الحد الأقصى لوقت انتظار استجابة الخادم. بعد هذا الوقت، يبلغ العميل عن خطأ timeout بإلقاء استثناء؛
  • الأسطر 41-46: مكتبة Google Guava المستخدمة في الاختبار JUnit. لهذا السبب، قمنا بتحديد نطاقها إلى [test] (السطر 45). وهذا يعني أن هذه التبعية لا يتم تضمينها إلا عند تنفيذ أكواد الفرع [src/test/java
  • الأسطر 48-51: مكتبة السجلات؛
  • الأسطر 52-63: التبعية الخاصة باختبارات JUnit. وهي تجلب على وجه الخصوص المكتبة JUnit 4 اللازمة للاختبارات. تحتوي هذه التبعيات على السمة [<scope>test</scope>] التي تشير إلى أنها ضرورية فقط لمرحلة الاختبارات. وهي غير مدرجة في الأرشيف النهائي للمشروع؛

13.6.3. تنفيذ الطبقة [DAO]

  
  • تحتوي الحزمة [spring.client.config] على تكوين Spring للطبقة [DAO
  • يحتوي الحزمة [spring.client.dao] على تنفيذ الطبقة [DAO
  • تحتوي الحزمة [spring.client.entities] على الكائنات المتبادلة مع خدمة الويب / jSON؛

13.6.3.1. Configuration

  

تقوم الفئة [DaoConfig] بإجراء تكوين Spring للطبقة [DAO]. وفيما يلي شفرتها:


package spring.client.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@ComponentScan({ "spring.client.dao" })
public class DaoConfig {

    // الثوابت
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // إنشاء المكون RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // مهلة انتهاء التبادل
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // النتيجة
        return restTemplate;
    }

    @Bean
    public int timeout() {
        return TIMEOUT;
    }

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }

    // مرشحات jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // أداة التعيين jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // النتيجة
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // مُخطِط jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // النتيجة
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // مُخطِط jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // النتيجة
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // مُخطِط jSON
        ObjectMapper mapper = new ObjectMapper();
        // مرشحات
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // النتيجة
        return mapper;
    }
}
  • السطر 13: الفئة هي فئة تكوين Spring - يجب البحث عن مكونات Spring في الحزمة [spring.client.dao
  • السطر 17: يتم تعيين timeout لمدة ثانية واحدة (1000 مللي ثانية)؛
  • الأسطر 32-35: الفول الذي يعرض هذه القيمة؛
  • السطر 18: URL لخدمة الويب / jSON؛
  • الأسطر 37-40: الكائن الذي يعرض هذه القيمة؛
  • الأسطر 20-30: تكوين الفئة [RestTemplate] التي تضمن التبادل مع خدمة الويب / jSON. عندما لا يتعين تكوينها، يمكن استخدامها في الكود ببساطة عن طريق [new RestTemplate()]. هنا، نريد تعيين timeout للتبادل مع خدمة الويب / jSON. يتم تمرير bean [timeout] في السطر 36 كمعلمة لطريقة [restTemplate] في السطر 24؛
  • السطر 23: المكون [HttpComponentsClientHttpRequestFactory] هو المكون الذي يسمح لنا بتحديد timeout للتبادل (السطران 29-30)؛
  • السطر 24: تم إنشاء الفئة [RestTemplate] باستخدام هذا المكون. ونظرًا لأنها تعتمد عليه للتواصل مع خدمة الويب / jSON، فسيتم إخضاع التبادلات بالفعل لـ timeout؛
  • سيتبادل العميل والخادم أسطر نصية. يتولى المحول تحويل كائن إلى نص والعكس بالعكس، أي تحويل نص إلى كائن. قد يكون هناك عدة محولات مرتبطة بالفئة [RestTemplate]، ويعتمد اختيار المحول في لحظة معينة على الرؤوس HTTP المرسلة من الخادم. هنا، لن يكون لدينا أي محول. لذلك، لن يحاول المكون [RestTemplate] تحويل العنصرين التاليين بأي شكل من الأشكال:
    • النص المنشور؛
    • النص المستلم كرد؛

ستكون هذه النصوص سلاسل jSON والتي سيتم تركها كما هي بواسطة المكون [RestTemplate]. نحن كمطورين، سنقوم بعمليات التسلسل / إزالة التسلسل jSON اللازمة. وذلك لأن المرشحات التي سيتم تطبيقها على القيمة المرسلة والرد المستلم قد تكون مختلفة، وتُظهر التجربة أنه من الأسهل إدارتها بنفسك بدلاً من محاولة تكوين المكون [RestTemplate] بحيث يستخدم المحول الصحيح jSON؛

  • الأسطر 42-92: تحدد مرشحات jSON. وهي نفس المرشحات الموجودة في الخادم والموضحة في الفقرة 13.5.3.1؛
  • الأسطر 43-46: مُعِد خرائط jSON بدون مرشحات؛
  • الأسطر 64-68: مُعِدّ قوالب jSON لإنشاء فئة بدون منتجاتها؛
  • الأسطر 48-58: مخطط jSON للحصول على فئة مع منتجاتها؛
  • الأسطر 83-92: مخطط jSON للحصول على منتج بدون فئته؛
  • الأسطر 60-70: مخطط jSON للحصول على منتج مع فئته؛

ستكون جميع هذه الفاصوليا متاحة في أكواد الطبقة [DAO] وكذلك في اختبار Junit.

13.6.3.2. الكيانات

  

الكيانات التي تعالجها الطبقة [DAO] هي تلك التي تتبادلها مع خدمة الويب / jSON. وهي المقالات والمنتجات. على جانب الخادم، كانت هذه الكيانات تحتوي على تعليقات توضيحية خاصة بالاستمرارية JPA. هنا، تمت إزالة هذه التعليقات التوضيحية. نعيد عرض كود الكيانات للتذكير:

[AbstractEntity]


package spring.client.entities;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public abstract class AbstractEntity {
    // خصائص
    protected Long id;
    protected Long version;

    // المنشئون
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // إعادة تعريف [equals] و [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id == other.id.longValue();
    }

    // التوقيع jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    // مُستردات ومُعيّنات
...
}

[Categorie]


package spring.client.entities;

import java.util.HashSet;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {

    // الخصائص
    private String nom;

    // المنتجات ذات الصلة
    public Set<Produit> produits = new HashSet<Produit>();

    // المنشئات
    public Categorie() {

    }

    public Categorie(String nom) {
        this.nom = nom;
    }

    // الأساليب
    public void addProduit(Produit produit) {
        // إضافة المنتج
        produits.add(produit);
        // تحديد فئته
        produit.setCategorie(this);
    }

    // مُستردات ومُعيّنات
    ...
}

[Produit]


package spring.webjson.client.entities;

import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {

    // الاسم
    private String nom;
   // رقم الفئة
    private Long idCategorie;
   // السعر
    private double prix;
   // الوصف
    private String description;

    // الفئة
    private Categorie categorie;

    // الشركات المصنعة
    public Produit() {

    }

    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }

    // المُستقبلات والمُعيّنات
...
}

13.6.3.3. الفئة [DaoException]

 

عندما تواجه الطبقة [DAO] خطأً، فإنها ستطلق استثناءً من النوع [DaoException]. هذه الفئة هي تلك المستخدمة على جانب الخادم والموصوفة في الفقرة 11.3.7.

13.6.3.4. واجهة الطبقة [DAO]

 

تقدم الطبقة [DAO] واجهة [IDao] الموصوفة في الفقرة 11.3.7.


package spring.client.dao;

import java.util.List;

import spring.client.entities.Categorie;
import spring.client.entities.Produit;

public interface IDao {

    // إدراج قائمة المنتجات
    public List<Produit> addProduits(List<Produit> produits);

    // حذف جميع المنتجات
    public void deleteAllProduits();

    // تحديث قائمة المنتجات
    public List<Produit> updateProduits(List<Produit> produits);

    // الحصول على جميع المنتجات
    public List<Produit> getAllProduits();

    // إدراج قائمة بالفئات
    public List<Categorie> addCategories(List<Categorie> categories);

    // حذف جميع الفئات
    public void deleteAllCategories();

    // تحديث قائمة الفئات
    public List<Categorie> updateCategories(List<Categorie> categories);

    // الحصول على جميع الفئات
    public List<Categorie> getAllCategories();

    // منتج معين
    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // فئة معينة
    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);

}

13.6.3.5. استجابة خدمة الويب / jSON

  

لقد رأينا أن جميع URL لخدمة الويب / jSON تعطي نوعًا [Response] المحدد في الفقرة 13.5.5.3. نعيد هنا هذه الفئة:


package spring.client.dao;

import java.util.List;

public class Response<T> {

    // ----------------- الخصائص
    // حالة العملية
    private int status;
    // رسائل الخطأ المحتملة
    private List<String> messages;
    // نص الرد
    private T body;

    // المنشئات
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // مُستردات ومُعيّنات
    ...
}

13.6.3.6. تنفيذ التبادلات مع خدمة الويب / jSON

  

تنفذ الفئة [AbstractDao] التبادلات مع خدمة الويب / jSON:


package spring.client.dao;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestTemplate;

public abstract class AbstractDao {

    // البيانات
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // الاستعلام العام
    protected String getResponse(String url, String jsonPost) {

        // url: URL للاتصال
        // jsonPost: القيمة jSON المطلوب إرسالها
        try {
            // تنفيذ الطلب
            RequestEntity<?> request;
            if (jsonPost != null) {
                // الطلب POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // الطلب GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // يتم تنفيذ الاستعلام
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • السطران 15-16: إدخال المكون [RestTemplate] الذي يضمن الاتصال بالخادم؛
  • السطران 17-18: إدخال المكون URL لخدمة الويب / jSON؛

تم تجميع تنفيذ طرق الاتصال بالخادم في الطريقة [getResponse]:

  • السطر 21: تتلقى الطريقة معلمتين:
    • [url]: URL المطلوب؛
    • [jsonPost]: السلسلة jSON المراد نشرها، وإلا null. إذا كان [jsonPost==null]، يتم إجراء طلب URL باستخدام GET، وإلا باستخدام POST؛
  • السطر 38: التعليمات التي ترسل الطلب إلى الخادم وتتلقى الرد. يوفر المكون [RestTemplate] عددًا كبيرًا من طرق التبادل مع الخادم. وقد اخترنا هنا الطريقة [exchange]، ولكن هناك طرق أخرى؛
  • الأسطر 27-36: علينا إنشاء الطلب من النوع [RequestEntity]. ويختلف هذا الطلب باختلاف استخدام GET أو POST لإجراء الطلب؛
  • السطور 30-31: الاستعلام الخاص بـ GET. توفر الفئة [RequestEntity] طرقًا ثابتة لإنشاء الاستعلامات GET و POST و HEAD،... تسمح الطريقة [RequestEntity.get] بإنشاء طلب GET من خلال ربط الطرق المختلفة التي تبني هذا الطلب:
    • تقبل الطريقة [RequestEntity.get] كمعلمة الهدف URL في شكل مثيل URI،
    • تسمح الطريقة [accept] بتحديد عناصر الرأس HTTP [Accept]. هنا، نشير إلى أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
    • تستخدم الطريقة [build] هذه المعلومات المختلفة لإنشاء نوع [RequestEntity] للطلب؛
  • السطران 34-35: الطلب لـ POST. تسمح الطريقة [RequestEntity.post] بإنشاء طلب POST من خلال ربط الطرق المختلفة التي تبني هذا الطلب:
    • تقبل الطريقة [RequestEntity.post] كمعلمة الهدف URL في شكل مثيل URI،
    • تحدد الطريقة [header] رأسًا HTTP. هنا نرسل إلى الخادم رأس [Content-Type: application/json] لإعلامه بأن القيمة المرسلة ستصل إليه في شكل سلسلة jSON؛
    • تسمح الطريقة [accept] بالإشارة إلى أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
    • تحدد الطريقة [body] القيمة المرسلة. وهي المعلمة الرابعة للطريقة العامة [getResponse] (السطر 1)؛
  • السطر 38: تُرجع الطريقة [RestTemplate].exchange نوعًا [ResponseEntity<String>] الذي يغلف كامل استجابة الخادم: رؤوس HTTP ونص الوثيقة. تسمح الطريقة [ResponseEntity].getBody() بالحصول على هذا النص الذي يمثل استجابة الخادم، وهو هنا سلسلة من الأحرف؛

13.6.3.7. تنفيذ واجهة [IDao]

  

تنفذ الفئة [Dao] الواجهة [IDao]:


package spring.client.dao;

import java.io.IOException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import spring.client.entities.Categorie;
import spring.client.entities.Produit;

@Component
public class Dao extends AbstractDao implements IDao {

    @Autowired
    private ApplicationContext context;

    // تصفية jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
        // ----------- إضافة منتجات (بدون فئتها)
        ...
}
  • السطر 17: الفئة [Dao] هي مكون Spring يمكننا من خلاله حقن مكونات Spring أخرى؛
  • السطر 18: الفئة [Dao] تمتد الفئة [AbstractDao] التي رأيناها للتو وتنفذ الواجهة [IDao
  • السطران 20-21: يتم حقن سياق Spring للوصول إلى حبوبه؛
  • الأسطر 24-38: إدراج المُعَدِّلات jSON المُعرَّفة في الفئة [AppConfig] المُقدَّمة في الفقرة 13.6.2؛

تتبع جميع تطبيقات الطرق المختلفة لواجهة [IDao] نفس النمط. سنعرض طريقتين، إحداهما تعتمد على عملية [POST]، والأخرى على عملية [GET].

مثال على [GET]: [getCategorieByNameWithProduits]


@Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        // ----------- الحصول على فئة محددة بالاسم، مع منتجاتها
        try {
            // الاستعلام
            Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
                    new TypeReference<Response<Categorie>>() {
                    });
            // خطأ؟
            if (response.getStatus() != 0) {
                // يتم إلقاء استثناء واحد
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // يتم إرجاع جوهر استجابة الخادم
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(113, e2);
        }
    }
  • السطر 7: يتم استدعاء الطريقة [getResponse] من الفئة الأصلية. هذه الطريقة هي التي تضمن التبادل مع خدمة الويب / jSON. معلماتها هي كما يلي:

getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
  • (تابع)
    • URL للخدمة المستفسر عنها [/getCategorieByNameWithProduits/nom
    • القيمة المرسلة. لا توجد قيمة هنا؛

تُرجع الطريقة [getResponse] نوع String وهو الرد jSON المرسل من الخادم. يتم فك تسلسل هذا الرد jSON بالطريقة التالية:


jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<Categorie>>() {
});

لأن السلسلة jSON هي تسلسل لنوع [Response<Categorie>]؛

  • السطور 11-17: يتم اختبار حالة الرد. إذا كانت الحالة مختلفة عن 0، فهذا يعني أن هناك خطأ من جانب الخادم. عندئذ يتم إلقاء استثناء (السطر 13)، مع استرجاع المعلومات الواردة في الرد (الحالة وقائمة رسائل الخطأ)؛
  • السطر 16: إذا لم يكن هناك خطأ من جانب الخادم، يتم إرجاع نص النوع [Response<Categorie>]، أي الفئة المطلوبة؛
  • السطران 18-19: يتم التعامل مع الاستثناء الذي تم إطلاقه في السطر 16؛
  • السطور 20-22: معالجة جميع الاستثناءات الأخرى؛

مثال على [POST]: [addCategories]


@Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        // ----------- إضافة فئات (مع منتجاتها)
        try {
            // طلب
            Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
                    new TypeReference<Response<List<Categorie>>>() {
                    });
            // خطأ؟
            if (response.getStatus() != 0) {
                // يتم إلقاء استثناء واحد
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // يتم عرض جوهر استجابة الخادم
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(104, e2);
        }
    }
  • السطر 2: تُستخدم الطريقة [addCategories] لتخزين الفئات التي تم تمريرها كمعلمات في قاعدة البيانات. وهي تُثري هذه الفئات نفسها بمفاتيحها الأساسية. إذا تم تمرير الفئات مع منتجات، يتم تخزين هذه المنتجات أيضًا؛
  • السطر 7: يتم استدعاء الطريقة [getResponse] من الأصل لإجراء التبادلات مع خدمة الويب / jSON؛
    • المعلمة الأولى هي URL [/addCategories
    • المعلمة الثانية هي القيمة المرسلة، وهي هنا قائمة الفئات المطلوب الاحتفاظ بها؛

getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))

ثم يتم إزالة التسلسل من السلسلة jSON التي تم الحصول عليها للحصول على النوع المتوقع [Response<List<Categorie>]:


Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<List<Categorie>>>() {
                    });
  • الأسطر 11-17: إدارة استجابة الخادم (خطأ أم لا)؛
  • الأسطر 20-22: إدارة الاستثناءات؛

تتبع جميع الطرق الأخرى نمط الطريقتين المقدمتين.

13.6.4. اختبار JUnit

لنعد إلى بنية العميل/الخادم قيد الإنشاء:

لقد أنشأنا طبقة [DAO] [2] بنفس واجهة الطبقة [DAO] [4]. لاختبار الطبقة [DAO] [2]، يمكننا إذن استخدام الاختبار JUnit الذي استُخدم لاختبار الطبقة [DAO] [4]. للتذكير، هذا الاختبار هو التالي:

  

package spring.client.junit;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // طبقة [DAO]
    @Autowired
    private IDao dao;

    // مرشحات jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;

    @Before
    public void cleanAndFill() {
        // يتم تنظيف قاعدة البيانات قبل كل اختبار
        log("Vidage de la base de données", 1);
        // يتم إفراغ الجدول [CATEGORIES] - وبالتالي سيتم إفراغ الجدول [PRODUITS]
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // يتم ملء الجداول
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // إضافة الفئة - سيتم إدراج المنتجات أيضًا بشكل متسلسل
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // قائمة الفئات
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // قائمة المنتجات
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // بعض عمليات التحقق
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }

    @Test
    public void getCategorieByNameWithProduits() {
        log("getCategorieByNameWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals(5, categorie1.getProduits().size());
    }

    @Test
    public void getCategorieByNameWithoutProduits() {
        log("getCategorieByNameWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals("categorie1", categorie1.getNom());
    }

    @Test
    public void getCategorieByIdWithProduits() {
        log("getCategorieByIdWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
        Assert.assertNotNull(categorie2);
        Assert.assertEquals(categorie1.getId(), categorie2.getId());
        Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
    }

    @Test
    public void getCategorieByIdWithoutProduits() {
        log("getCategorieByIdWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
        Assert.assertNotNull(categorie2);
        Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
    }

    @Test
    public void getProduitByNameWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Assert.assertNotNull(produit);
        Assert.assertNotNull(produit.getCategorie());
    }

    @Test
    public void getProduitByNameWithoutCategorie() {
        log("getProduitByNameWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
        Assert.assertNotNull(produit);
        Assert.assertEquals("produit03", produit.getNom());
    }

    @Test
    public void getProduitByIdWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
        Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
    }

    @Test
    public void getProduitByIdWithoutCategorie() {
        log("getProduitByIdWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
    }

    @Test
    public void doInsertsInTransaction() {
        log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
        // نقوم بالإدراج
        Categorie categorie = new Categorie("cat1");
        categorie.addProduit(new Produit("x", 1.0, ""));
        categorie.addProduit(new Produit("x", 1.0, ""));
        // إضافة الفئة - سيتم إدراج المنتجات أيضًا بشكل متسلسل
        try {
            categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
        } catch (DaoException e) {
            show("Les erreurs suivantes se sont produites :", e.getErreurs());
        }
        // عمليات التحقق
        List<Categorie> categories = dao.getAllCategories();
        Assert.assertEquals(2, categories.size());
        List<Produit> produits = dao.getAllProduits();
        Assert.assertEquals(10, produits.size());
    }

    @Test
    public void updateDataBase() {
        log("Mise à jour du prix des produits de [categorie1]", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
        Set<Produit> produits = categorie1.getProduits();
        for (Produit produit : produits) {
            produit.setPrix(1.1 * produit.getPrix());
        }
        List<Produit> produits2 = Lists.newArrayList(produits);
        produits2 = dao.updateProduits(produits2);
        // عمليات التحقق
        List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
        for (Produit produit2 : produits2) {
            Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
            Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
        }
    }

    @Test
    public void addProduits() throws BeansException, JsonProcessingException {
        log("Ajout de deux produits de catégorie [categorie0]", 1);
        Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
        Long idCategorie = categorie0.getId();
        Produit p1 = new Produit("x", 1, "");
        p1.setIdCategorie(idCategorie);
        p1.setCategorie(categorie0);
        Produit p2 = new Produit("y", 1, "");
        p2.setIdCategorie(idCategorie);
        p2.setCategorie(categorie0);
        List<Produit> produits = new ArrayList<Produit>();
        produits.add(p1);
        produits.add(p2);
        produits = dao.addProduits(produits);
        // التحقق
        affiche(produits, jsonMapperProduitWithoutCategorie);
    }

    // -------------- طرق خاصة
    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }

    private Categorie findCategorieByName(String nom, List<Categorie> categories) {
        for (Categorie categorie : categories) {
            if (categorie.getNom().equals(nom)) {
                return categorie;
            }
        }
        return null;
    }

    // عرض عنصر من النوع T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // عرض قائمة بعناصر من النوع T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // عرض رسالة
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }

    private static void show(String title, List<String> messages) {
        // العنوان
        System.out.println(String.format("%s : ", title));
        // الرسائل
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}

تم تنفيذه بنجاح وأعطى النتائج التالية على وحدة التحكم:


Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites : 
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy