Skip to content

2. المادة 1 - Spring IoC

أهداف الوثيقة:

  • اكتشاف إمكانيات تكوين وتكامل إطار عمل Spring (http://www.springframework.org)
  • تحديد واستخدام مفهوم IoC (انعكاس التحكم)، المعروف أيضًا باسم حقن التبعية (Dependency Injection)

2.1. تكوين تطبيق ثلاثي الطبقات باستخدام Spring

لنأخذ تطبيقًا كلاسيكيًا ثلاثي الطبقات:

سنفترض أن الوصول إلى طبقات الأعمال و DAO يتم التحكم فيه بواسطة واجهات Java:

  1. الواجهة [IArticlesDao] لطبقة الوصول إلى البيانات
  1. الواجهة [IArticlesManager] لطبقة الأعمال

في طبقة الوصول إلى البيانات أو طبقة DAO (Data Access Object)، من الشائع العمل مع SGBD وبالتالي مع برنامج تشغيل JDBC. لنأخذ على سبيل المثال الهيكل الأساسي لفئة تصل إلى جدول المواد في SGBD:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // الاتصال بمصدر البيانات
    private String driverClassName=null;
    private Connection connexion=null;
    private String url = null;
    private String user = null;
    private String pwd = null;
 ....

    public List getAllArticles() {
        // طلب قائمة العناصر
        try {
             // تحميل برنامج التشغيل JDBC
            Class.forName(driverClassName);
            // إنشاء اتصال بـ BD
            connexion = DriverManager.getConnection(url, user, pwd);
            ...
        } catch (SQLException ex) {
            ...
        } finally {
            ...
        }
    }

لإجراء عملية على SGBD، تحتاج أي طريقة إلى كائن [Connection] يمثل الاتصال بقاعدة البيانات، والذي سيمر عبره التبادل بينها وبين كود Java. لإنشاء هذا الكائن، نحتاج إلى أربع معلومات:

String driverClassName
اسم فئة برنامج التشغيل JDBC لـ SGBD
String url
عنوان URL JDBC للقاعدة المطلوب استخدامها
String user
الهوية التي يتم من خلالها إنشاء الاتصال
String pwd
كلمة مرور هذه الهوية

كيف يمكن لفصلنا السابق [ArticlesDaoPlainJdbc] الحصول على هذه المعلومات؟ هناك عدة احتمالات:

الحل 1 - المعلومات مدمجة في الفئة:

1
2
3
4
5
6
7
8
public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // اتصال بمصدر البيانات
    private final String driverClassName = "org.firebirdsql.jdbc.FBDriver";
    private String url = "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb";
    private String user = "someone";
    private String pwd = "somepassword";
 ....

عيب هذا الحل هو أنه يتعين تعديل كود Java بمجرد حدوث أي تغيير في هذه المعلومات، مثل تغيير كلمة المرور.

الحل 2 - يتم نقل المعلومات إلى الكائن عند إنشائه:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // الاتصال بمصدر البيانات
    private final String driverClassName;
    private String url;
    private String user;
    private String pwd;
 ....
    public ArticlesDaoPlainJdbc(String driverClassName,String url,String user,String pwd) {
      this.driverClassName=driverClassName;
    this.url=url;
    this.user=user;
    this.pwd=pwd;
    ...
    }

هنا، يتلقى الكائن عند إنشائه المعلومات التي يحتاجها للعمل. وبالتالي، يتم تحويل المشكلة إلى الكود الذي أرسل له المعلومات الأربع. كيف حصل عليها؟ يمكن للفئة التالية [ArticlesManagerWithDataBase] من طبقة الأعمال إنشاء كائن [ArticlesDaoPlainJdbc] من طبقة الوصول إلى البيانات:

public class ArticlesManagerWithDataBase implements IArticlesManager {

    // مثيل للوصول إلى البيانات
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // إنشاء خدمة الوصول إلى البيانات
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

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

هناك مجموعة كبيرة ومتنوعة من ملفات التكوين. الاتجاه الحالي هو استخدام ملفات XML. هذا هو الخيار الذي اتخذته Spring. قد يكون الملف الذي يقوم بتكوين كائن [ArticlesDaoPlainJdbc] كما يلي:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة الوصول إلى البيانات -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
</beans>

التطبيق هو مجموعة من الكائنات التي يطلق عليها Spring اسم beans، لأنها تتبع معيار JavaBean لتسمية أدوات الوصول والمُهيئات (getters/setters) للحقول الخاصة بكائن ما. غالبًا ما يتم إنشاء الكائنات التي تقوم بدور تقديم خدمة في التطبيق بنسخة واحدة فقط. ويُطلق عليها اسم "الوحدات الفردية" (singletons). وهكذا، في مثال التطبيق متعدد المستويات الذي ندرسه هنا، سيتم توفير الوصول إلى قاعدة بيانات المقالات من خلال نسخة واحدة من فئة [ArticlesDaoPlainJdbc]. بالنسبة لتطبيق الويب، تخدم كائنات الخدمة هذه عدة عملاء في وقت واحد. ولا يتم إنشاء كائن خدمة لكل عميل.

يسمح ملف تكوين Spring أعلاه بإنشاء كائن خدمة فريد من النوع [ArticlesDaoPlainJdbc] في حزمة تسمى [istia.st.articles.dao]. يتم تعريف المعلومات الأربع اللازمة لمنشئ هذا الكائن داخل علامة <bean>...</bean>. سيكون لدينا عدد من علامات <bean> يساوي عدد الكائنات الفردية المطلوب إنشاؤها.

متى سيتم إنشاء الكائنات المحددة في ملف Spring؟ يمكن أن يتم تهيئة التطبيق من خلال طريقة main الخاصة بهذا التطبيق نفسه إذا كان يحتوي على واحدة. بالنسبة لتطبيق الويب، قد تكون هذه هي طريقة [init] الخاصة بالسيرفلت الرئيسي. يوجد في كل تطبيق طريقة من المؤكد أنها ستكون أول ما يتم تنفيذه. وعادةً ما يتم إنشاء الكائنات الفردية في هذه الطريقة.

لنأخذ مثالاً. لنفترض أننا نريد اختبار الفئة [ArticlesDaoPlainJdbc] السابقة باستخدام اختبار JUnit. تحتوي فئة الاختبار JUnit على طريقة [setUp] يتم تنفيذها قبل أي طريقة أخرى. وهنا يتم إنشاء العنصر الفردي [ArticlesDaoPlainJdbc].

إذا اتبعنا الحل المتمثل في تمرير معلومات التكوين عبر المنشئ، فسنحصل على فئة الاختبار التالية:

public class TestArticlesPlainJdbc extends TestCase {
    // اختبار فئة الوصول إلى العناصر ArticlesDaoPlainJdbc
     // مصدر البيانات محدد في sprintest

     // مثيل للفئة التي يتم اختبارها
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception{
        // يسترد مثيل الوصول إلى البيانات
        articlesDao =
            (IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

يجب أن تعرف فئة الاستدعاء [TestArticlesPlainJdbc] المعلومات الأربع اللازمة لتهيئة العنصر الفردي [ArticlesDaoPlainJdbc] المراد إنشاؤه.

إذا اتبعنا الحل المتمثل في تمرير معلومات التكوين عبر ملف التكوين، فقد نحصل على فئة الاختبار التالية باستخدام ملف Spring الموصوف أعلاه.

public class TestSpringArticlesPlainJdbc extends TestCase {
    // يختبر فئة الوصول إلى العناصر ArticlesDaoJdbc
     // مصدر البيانات محدد في sprintest

     // مثيل للفئة التي يتم اختبارها
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
      // يسترد مثيل الوصول إلى البيانات
      articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesPlainJdbc.xml"))).getBean("articlesDao");
    }

هنا، لا تحتاج فئة الاستدعاء [TestSpringArticlesPlainJdbc] إلى معرفة المعلومات اللازمة لتهيئة العنصر الفردي الذي سيتم إنشاؤه. إنها تحتاج فقط إلى معرفة:

  1. [springArticlesPlainJdbc.xml]: اسم ملف تكوين Spring الموصوف أعلاه
  2. [articlesDao]: اسم الكائن الفردي المراد إنشاؤه

لا يؤثر أي تعديل في ملف التكوين، بخلاف هذين العنصرين، على كود Java. هذه الطريقة لتكوين كائنات التطبيق مرنة للغاية. للتكوين، لا تحتاج هذه الطريقة إلا إلى معرفة أمرين:

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

2.2. حقن التبعية وعكس التحكم

لنقدم الآن مفهوم حقن التبعية (Dependency Injection) الذي يستخدمه Spring لتكوين التطبيقات. كما يستخدم مصطلح انعكاس التحكم (IoC، Inversion of Control). لننظر إلى إنشاء الكائن الفردي [ArticlesManagerWithDataBase] في الطبقة الوظيفية لتطبيقنا:

للوصول إلى بيانات SGBD، يجب أن تستخدم طبقة الأعمال خدمات كائن ينفذ واجهة [IArticlesDao]، على سبيل المثال كائن من النوع [ArticlesDaoPlainJdbc]. قد يبدو كود فئة [ArticlesManagerWithDataBase] كما يلي:

public class ArticlesManagerWithDataBase implements IArticlesManager {

     // مثيل للوصول إلى البيانات
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // إنشاء خدمة الوصول إلى البيانات
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

من المفترض أن تقوم الفئة [ArticlesDaoPlainJdbc] هنا بتنفيذ واجهة [IArticlesDao]:

public class ArticlesDaoPlainJdbc implements IArticlesDao {...}

لإنشاء العنصر الفردي من النوع [IArticlesDao] الضروري لتشغيل الفئة، يستخدم منشئ هذه الفئة بشكل صريح اسم فئة تنفيذ الواجهة [IArticlesDao]:

articlesDao =(IArticlesDao) new ArticlesDaoPlainJdbc(...);

وبالتالي، لدينا تبعيات ثابتة في الكود على اسم الفئة. إذا تغيرت فئة تنفيذ الواجهة [IArticlesDaoفسيتعين تعديل كود المنشئ السابق. لدينا العلاقات التالية بين الكائنات:

تقوم الفئة [ArticlesManagerWithDataBase] بنفسها بإنشاء الكائن [ArticlesDaoPlainJdbc] الذي تحتاجه. وبالعودة إلى مصطلح "عكس التحكم"، يمكن القول إنها هي التي تمتلك "التحكم" لإنشاء الكائن الذي تحتاجه.

إذا كان علينا كتابة فئة اختبار JUnit لفئة [ArticlesManagerWithDataBase]، فقد يكون لدينا ما يلي:

public class TestArticlesManagerWithDataBase extends TestCase {
    // مثيل للفئة التجارية التي يتم اختبارها
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
        // إنشاء مثيل لفئة الأعمال التي تم اختبارها
        articlesManager =
            (IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

تقوم فئة الاختبار بإنشاء مثيل لفئة الأعمال [ArticlesManagerWithDataBase] التي تقوم بدورها، في منشئها، بإنشاء مثيل لفئة الوصول إلى البيانات [ArticlesDaoPlainJdbc].

ستقضي الحلول التي تعتمد على Spring على حاجة فئة الأعمال [ArticlesManagerWithDataBase] إلى معرفة اسم فئة الوصول إلى البيانات [ArticlesDaoPlainJdbc] التي تحتاجها. وهذا سيسمح بتغييرها دون المساس برمز جافا الخاص بفئة الأعمال. سيسمح Spring بإنشاء كل من العنصرين الفرديين في نفس الوقت، أي عنصر طبقة الوصول إلى البيانات وعنصر طبقة الأعمال. سيحدد ملف تكوين Spring حبة جديدة:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة الوصول إلى البيانات -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
</beans>

يكمن الجديد في bean الذي يحدد singleton لفئة الأعمال المطلوب إنشاؤها:

    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
  1. تم تعريف الفئة التي تنفذ bean [articlesManager]: [ArticlesManagerWithDataBase]
  2. يتم تعيين قيمة للحقل [articlesDao] في bean بواسطة العلامة <property name="articlesDao">. هذا هو الحقل المحدد في الفئة [ArticlesManagerWithDataBase]:
public class ArticlesManagerWithDataBase implements IArticlesManager {

  // واجهة الوصول إلى البيانات
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

لكي يتم تهيئة الحقل [articlesDao] بواسطة Spring وعلامته <property>، يجب أن يتبع الحقل المعيار JavaBean وأن توجد طريقة [setArticlesDao] لتهيئة الحقل [articlesDao]. تجدر الإشارة إلى اسم الطريقة، المشتق بشكل دقيق من اسم الحقل. بالتوازي مع ذلك، غالبًا ما توجد طريقة [get...] للحصول على قيمة الحقل. هنا، هي الطريقة [getArticlesDao]. في هذه النسخة الجديدة، لم يعد للفئة [ArticlesManagerWithDataBase] منشئ. لم تعد بحاجة إليه.

  • القيمة التي سيتم تعيينها للحقل [articlesDao] بواسطة Spring هي قيمة bean [articlesDao] المحددة في ملف التكوين الخاص بها:
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
    .............
    </bean>
  • عندما يقوم Spring بإنشاء الكائن الفردي [ArticlesManagerWithDataBase]، سيضطر أيضًا إلى إنشاء الكائن الفردي [ArticlesDaoPlainJdbc]:
    • سيقوم Spring بإنشاء مخطط تبعيات للـ beans وسيلاحظ أن الـ bean [articlesManager] يعتمد على الـ bean [articlesDao]
    • وسيقوم بإنشاء bean [articlesDao]، أي كائن من النوع [ArticlesDaoPlainJdbc]
    • ثم سيقوم بإنشاء bean [articlesManager] من النوع [ArticlesManagerWithDataBase]

لنتخيل الآن اختبار JUnit للفئة [ArticlesManagerWithDataBase]. قد يبدو كما يلي:

public class TestSpringArticlesManagerWithDataBase extends TestCase {
    // يختبر فئة الأعمال [ArticlesManagerWithDataBase]

    // مثيل لفئة الأعمال التي تم اختبارها
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
      // يسترد مثيل الوصول إلى البيانات
      articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
    }

دعونا نتابع عملية إنشاء العنصرين الفرديين المحددين في ملف Spring المسمى [springArticlesManagerWithDataBase.xml].

  • تطلب الطريقة [setUp] أعلاه مرجعًا للبيان المسمى [articlesManager]
  • يقوم Spring بالرجوع إلى ملف التكوين الخاص به، ويجد bean [articlesManager]. إذا كان قد تم إنشاؤه بالفعل، فإنه يكتفي بإرجاع مرجع إلى الكائن (السينجلتون)، وإلا فإنه يقوم بإنشائه.
  • يرى Spring تبعية bean [articlesManager] لـ bean [articlesDao]. لذلك يقوم بإنشاء الكائن الفردي (singleton) [articlesDao] من النوع [ArticlesDaoPlainJdbc] إذا لم يكن قد تم إنشاؤه بالفعل (كائن فردي).
  • ويقوم بإنشاء الكائن الفردي [articlesManager] من النوع [ArticlesManagerWithDataBase]

يمكن توضيح هذه الآلية كما يلي:

لنتذكر هيكل الفئة [ArticlesManagerWithDataBase]:

public class ArticlesManagerWithDataBase implements IArticlesManager {

  // واجهة الوصول إلى البيانات
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

في نهاية إنشاء العناصر الفردية بواسطة Spring، يكون لدينا كائن من النوع [ArticlesManagerWithDataBase] الذي تم تهيئة حقل [articlesDao] الخاص به دون أن يعرف كيف. يُقال إننا قمنا بحقن التبعية في الكائن [ArticlesManagerWithDataBase]. يُقال أيضًا أنه تم عكس التحكم: لم يعد الكائن [ArticlesManagerWithDataBase] هو الذي يأخذ زمام المبادرة لإنشاء الكائن الذي ينفذ الواجهة [IArticlesDao] التي يحتاجها بنفسه، بل أصبحت التطبيق على أعلى مستوى (عند تهيئته) هو الذي يتولى إنشاء جميع الكائنات التي يحتاجها من خلال إدارة التبعيات بينها.

الميزة الرئيسية لتكوين العنصر الفردي [ArticlesManagerWithDataBase] بواسطة ملف Spring، هي أنه يمكن الآن تغيير فئة التنفيذ المطابقة للحقل [articlesDao] في الفئة [ArticlesManagerWithDataBase] دون تعديل كود هذه الأخيرة. يكفي تغيير اسم الفئة في تعريف bean [articlesDao] في ملف Spring:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
...
    </bean>

سيصبح على سبيل المثال:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoIbatisSqlMap">
...
    </bean>

سيعمل bean [ArticlesManagerWithDataBase] مع فئة الوصول إلى البيانات الجديدة هذه، دون أن يدرك ذلك.

2.3. Spring IoC من الناحية العملية

2.3.1. مثال 1

لنأخذ الفئة التالية بعين الاعتبار:

package istia.st.springioc.domain;

public class Personne {
  private String nom;
  private int age;

   // عرض الشخص
  public String toString() {
    return "nom=[" + this.nom + "], age=[" + this.age + "]";
  }

   // init-close
  public void init() {
    System.out.println("init personne [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy personne [" + this.toString() + "]");
  }

   // getters-setters
  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
    this.nom = nom;
  }
}

تحتوي الفئة على:

  • حقلين خاصين هما الاسم والعمر
  • طريقتي القراءة (get) والكتابة (set) لهذين الحقلين
  • طريقة toString لاسترداد قيمة الكائن [Personne] في شكل سلسلة أحرف
  • طريقة init التي سيتم استدعاؤها بواسطة Spring عند إنشاء الكائن، وطريقة close التي سيتم استدعاؤها عند إتلاف الكائن

لإنشاء كائنات من النوع [Personne]، سنستخدم ملف Spring التالي:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
</beans>

سيُسمى هذا الملف config.xml.

  • وهو يحدد حبتين بمفاتيح "personne1" و"personne2" من النوع [Personne]
  • يقوم بتهيئة الحقول [nom, age] لكل شخص
  • يحدد الطرق التي يجب استدعاؤها عند الإنشاء الأولي للكائن [init-method] وعند إتلاف الكائن [destroy-method]

لأغراض الاختبار، سنستخدم فئة اختبار واحدة هي JUnit وسنضيف إليها طرقًا تباعًا. ستكون النسخة الأولى من هذه الفئة كما يلي:

package istia.st.springioc.tests;

import istia.st.springioc.domain.Personne;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

public class Tests extends TestCase {

   // مصنع الفاصوليا
  private ListableBeanFactory bf;

   // اختبارات التهيئة
  public void setUp() {
    bf = new XmlBeanFactory(new ClassPathResource("config.xml"));
  }

  public void test1() {
    // استرداد الفاصوليا باستخدام مفتاحها [Personne] من ملف Spring
    Personne personne1 = (Personne) bf.getBean("personne1");
    System.out.println("personne1=" + personne1.toString());
    Personne personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
  }
}

تعليقات:

  • للحصول على الحبوب المحددة في الملف [config.xml]، نستخدم كائنًا من النوع [ListableBeanFactory]. هناك أنواع أخرى من الكائنات التي تسمح بالوصول إلى الحبوب. يتم الحصول على الكائن [ListableBeanFactory] في الطريقة [setUp] من فئة الاختبار ويتم تخزينه في متغير خاص. وبذلك سيكون متاحًا لجميع طرق الاختبار.
  • سيتم وضع الملف [config.xml] في [ClassPath] للتطبيق، c.a.d. في أحد الدلائل التي تستكشفها الآلة الافتراضية Java عند البحث عن فئة مشار إليها من قبل التطبيق. يُستخدم الكائن [ClassPathResource] للبحث عن مورد في ملف [ClassPath] الخاص بتطبيق ما، وهو في هذه الحالة الملف [config.xml].
  • يمكن لـ Spring استخدام ملفات تكوين ذات تنسيقات متنوعة. يسمح الكائن [XmlBeanFactory] بتحليل ملف تكوين بتنسيق XML.
  • يؤدي تشغيل ملف Spring إلى إنشاء كائن من النوع [ListableBeanFactory]، وهو هنا الكائن bf. باستخدام هذا الكائن، يمكن الحصول على bean المحدد بالمفتاح C عن طريق bf.getBean(C).
  • تطلب الطريقة [test1] وتعرض قيمة الحبوب ذات المفتاح "personne1" و"personne2".

هيكل مشروع Eclipse لتطبيقنا هو كما يلي:

Image

تعليقات:

  • يحتوي المجلد [src] على أكواد المصدر. ستنتقل الأكواد المُجمَّعة إلى مجلد [bin] غير الموضح هنا.
  • يوجد الملف [config.xml] في جذر المجلد [src]. يؤدي إنشاء المشروع إلى نسخه تلقائيًا إلى المجلد [bin]، الذي يشكل جزءًا من [ClassPath] الخاص بالتطبيق. وهنا يتم البحث عنه بواسطة الكائن [ClassPathResource].
  • يحتوي المجلد [lib] على ثلاث مكتبات Java ضرورية للتطبيق:
      • commons-logging.jar و spring-core.jar لفئات Spring
      • junit.jar لفئات JUnit
  • المجلد [lib] هو أيضًا جزء من [ClassPath] الخاص بالتطبيق

يؤدي تنفيذ الطريقة [test1] للاختبار JUnit إلى النتائج التالية:

18 sept. 2004 11:28:53 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
personne1=nom=[Simon], age=[40]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
personne2=nom=[Brigitte], age=[20]
personne2=nom=[Brigitte], age=[20]

تعليقات:

  • يقوم Spring بتسجيل عدد من الأحداث بفضل المكتبة [commons-logging.jar]. تسمح لنا هذه السجلات بفهم أفضل لكيفية عمل Spring.
  • تم تحميل الملف [config.xml] ثم استخدامه
  • العملية*
Personne personne1 = (Personne) bf.getBean("personne1");

أجبرت على إنشاء bean [personne1]. يمكننا رؤية سجل Spring بهذا الشأن. نظرًا لأننا كتبنا [init-method="init"] في تعريف bean [personne1]، فقد تم تنفيذ الطريقة [init] للكائن [Personne] الذي تم إنشاؤه. يتم عرض الرسالة المقابلة.

  • العملية
System.out.println("personne1=" + personne1.toString());

أدت إلى عرض قيمة الكائن [Personne] الذي تم إنشاؤه.

  • تكرر نفس الظاهرة بالنسبة لفول المفتاح [personne2].
  • العملية الأخيرة
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());

لم تؤدِ إلى إنشاء كائن جديد من النوع [Personne]. لو كان الأمر كذلك، لكانت طريقة [init] قد عُرضت، وهو ما لم يحدث هنا. هذا هو مبدأ الكائن الفردي (singleton). لا يقوم Spring، بشكل افتراضي، بإنشاء سوى نسخة واحدة من حبوب ملف التكوين الخاص به. إنه خدمة مراجع الكائنات. إذا طُلب منه مرجع كائن لم يتم إنشاؤه بعد، فإنه يقوم بإنشائه ويقدم مرجعًا له. إذا كان الكائن قد تم إنشاؤه بالفعل، يكتفي Spring بتقديم مرجع له.

  • يمكن ملاحظة أنه لا يوجد أي أثر للطريقة [close] للكائن [Personne]، في حين أننا كتبنا في تعريف حبوب [destroy-method=close]. من المحتمل ألا يتم تنفيذ هذه الطريقة إلا عندما يتم استرداد الذاكرة التي يشغلها الكائن بواسطة أداة جمع القمامة (garbage collector). في اللحظة التي يحدث فيها ذلك، يكون التطبيق قد انتهى بالفعل ولا يكون للكتابة على الشاشة أي تأثير. يجب التحقق من ذلك.

بعد أن اكتسبنا الآن أساسيات تكوين Spring، سنكون أسرع قليلاً في شرحنا.

2.3.2. المثال 2

لنأخذ الفئة الجديدة [Voiture] التالية:

package istia.st.springioc.domain;

public class Voiture {
  private String marque;
  private String type;
  private Personne propriétaire;

   // المنشئات

  public Voiture() {
  }

  public Voiture(String marque, String type, Personne propriétaire) {
    this.marque = marque;
    this.type = type;
    this.propriétaire = propriétaire;
  }

   // toString
  public String toString() {
    return "Voiture : marque=[" + this.marque + "] type=[" + this.type
        + "] propriétaire=[" + this.propriétaire + "]";
  }

     // مُستردات-مُعيّنات
  public String getMarque() {
    return marque;
  }

  public void setMarque(String marque) {
    this.marque = marque;
  }

  public Personne getPropriétaire() {
    return propriétaire;
  }

  public void setPropriétaire(Personne propriétaire) {
    this.propriétaire = propriétaire;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

   // التشغيل والإغلاق
  public void init() {
    System.out.println("init voiture [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy voiture [" + this.toString() + "]");
  }

}

تحتوي الفئة على:

  • ثلاثة حقول خاصة هي type و marque و propriétaire. يمكن تهيئة هذه الحقول وقراءتها بواسطة طرق عامة من الفاصوليا get و set. كما يمكن تهيئتها باستخدام المنشئ Voiture(String, String, Personne). تحتوي الفئة أيضًا على منشئ بدون حجج من أجل اتباع المعيار JavaBean.
  • طريقة toString لاسترداد قيمة الكائن [Voiture] في شكل سلسلة أحرف
  • طريقة init التي سيتم استدعاؤها بواسطة Spring مباشرة بعد إنشاء الكائن، وطريقة close التي سيتم استدعاؤها عند تدمير الكائن

لإنشاء كائنات من النوع [Voiture]، سنستخدم ملف Spring [config.xml] التالي:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <constructor-arg index="0">
            <value>Peugeot</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>307</value>
        </constructor-arg>
        <constructor-arg index="2">
            <ref bean="personne2"></ref>
        </constructor-arg>
    </bean>
</beans>

يضيف هذا الملف إلى التعريفات السابقة حبة (bean) بمفتاح "voiture1" من النوع [Voiture]. لتهيئة هذه الحبة، كان بإمكاننا كتابة:

    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <property name="marque">
            <value>Peugeot</value>
        </property>
        <property name="type">
            <value>307</value>
        </property>
        <property name="propriétaire">
            <ref bean="personne2"/>
        </property>
    </bean>

بدلاً من اختيار هذه الطريقة التي سبق عرضها، اخترنا هنا استخدام منشئ Voiture(String, String, Personne) الخاص بالفئة. بالإضافة إلى ذلك، يحدد bean [voiture1] الطريقة التي يجب استدعاؤها عند الإنشاء الأولي للكائن [init-method] وتلك التي يجب استدعاؤها عند تدمير الكائن [destroy-method].

لإجراء اختباراتنا، سنستخدم فئة الاختبار JUnit التي تم عرضها سابقًا، مع إضافة الطريقة [test2] التالية إليها:

1
2
3
4
5
  public void test2() {
     // استرداد الفول [voiture1]
    Voiture Voiture1 = (Voiture) bf.getBean("voiture1");
    System.out.println("Voiture1=" + Voiture1.toString());
  }

تسترد الطريقة [test2] الحبة [voiture1] وتعرضها.

تظل بنية مشروع Eclipse كما كانت في الاختبار السابق. يؤدي تنفيذ الطريقة [test2] من الاختبار JUnit إلى النتائج التالية:

18 sept. 2004 14:56:10 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'voiture1'
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'voiture1' instantiated via constructor [public istia.st.springioc.domain.Voiture(java.lang.String,java.lang.String,istia.st.springioc.domain.Personne)]
init voiture [Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]]
Voiture1=Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]

تعليقات:

  1. تطلب الطريقة [test2] مرجعًا إلى bean [voiture1]
  2. السطر 4: يبدأ Spring في إنشاء bean [voiture1] لأن هذا bean لم يتم إنشاؤه بعد (singleton)
  3. السطر 6: نظرًا لأن bean [voiture1] يشير إلى bean [personne2]، يتم إنشاء هذا الأخير بدوره
  4. السطر 7: تم إنشاء bean [personne2]. ثم يتم تنفيذ طريقة [init] الخاصة به.
  5. السطر 9: يشير Spring إلى أنه سيستخدم منشئًا لإنشاء bean [voiture1]
  6. السطر 10: تم إنشاء bean [voiture1]. ثم يتم تنفيذ طريقة [init] الخاصة به.
  7. السطر 11: تعرض الطريقة [test2] قيمة bean [voiture1]

2.3.3. المثال 3

نقوم بإدخال الفئة الجديدة [GroupePersonnes] التالية:

package istia.st.springioc.domain;

import java.util.Map;

public class GroupePersonnes {
  private Personne[] membres;
  private Map groupesDeTravail;

   // المُستردات - المُعيّنات
  public Personne[] getMembres() {
    return membres;
  }

  public void setMembres(Personne[] membres) {
    this.membres = membres;
  }

  public Map getGroupesDeTravail() {
    return groupesDeTravail;
  }

  public void setGroupesDeTravail(Map groupesDeTravail) {
    this.groupesDeTravail = groupesDeTravail;
  }

   // العرض
  public String toString() {
    String liste = "membres : ";
    for (int i = 0; i < this.membres.length; i++) {
      liste += "[" + this.membres[i].toString() + "]";
    }
    return liste + ", groupes de travail = " + this.groupesDeTravail.toString();
  }

   // init-close
  public void init() {
    System.out.println("init GroupePersonnes [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy GroupePersonnes [" + this.toString() + "]");
  }
}

عضواها الخاصان هما:

الأعضاء: مصفوفة تضم الأشخاص الأعضاء في المجموعة

groupesDeTravail: قاموس يربط شخصًا بمجموعة عمل

يُلاحظ هنا أن الفئة [GroupePersonnes] لا تحدد منشئًا بدون حجة لاتباع المعيار JavaBean. نذكر أنه في حالة عدم وجود أي منشئ، يوجد منشئ "افتراضي" وهو المنشئ بدون حجج والذي لا يقوم بأي شيء.

نسعى هنا إلى إظهار كيف يتيح Spring تهيئة كائنات معقدة مثل الكائنات التي تحتوي على حقول من نوع المصفوفة أو القاموس. نضيف حبة جديدة إلى ملف Spring [config.xml] السابق:

    <bean id="groupe1" class="istia.st.springioc.domain.GroupePersonnes" 
        init-method="init" destroy-method="close">
        <property name="membres">
            <list>
                <ref bean="personne1"/>
                <ref bean="personne2"/>
            </list>
        </property>
        <property name="groupesDeTravail">
            <map>
                <entry key="Brigitte">
                    <value>Marketing</value>
                </entry>
                <entry key="Simon">
                    <value>Ressources humaines</value>
                </entry>
            </map>
        </property>
    </bean>
  1. تسمح العلامة <list> بتهيئة حقل من نوع صفيف أو ينفذ واجهة List بقيم مختلفة.
  2. تسمح العلامة <map> بالقيام بنفس الشيء مع حقل ينفذ واجهة Map

لإجراء اختباراتنا، سنستخدم فئة الاختبار JUnit التي تم عرضها سابقًا، مع إضافة الطريقة [test3] التالية إليها:

1
2
3
4
5
  public void test3() {
    // استرداد الفول [groupe1]
    GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
    System.out.println("groupe1=" + groupe1.toString());
  }

تسترد الطريقة [test3] الحبة [groupe1] وتعرضها.

تظل بنية مشروع Eclipse كما كانت في الاختبار السابق. يؤدي تنفيذ الطريقة [test3] من الاختبار JUnit إلى النتائج التالية:

18 sept. 2004 15:51:45 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'groupe1'
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
init GroupePersonnes [membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}]
groupe1=membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}

تعليقات:

  • تطلب الطريقة [test3] مرجعًا من bean [groupe1]
  • السطر 4: يبدأ Spring في إنشاء هذا bean
  • لأن bean [groupe1] يشير إلى bean [personne1] و [personne2]، يتم إنشاء هذين bean (السطران 6 و 9) وتنفيذ طريقة init الخاصة بهما (السطران 7 و 10)
  • السطر 11: تم إنشاء bean [groupe1]. يتم الآن تنفيذ طريقة [init] الخاصة به.
  • السطر 12: عرض مطلوب بواسطة الطريقة [test3].

2.4. Spring لتكوين تطبيقات الويب ثلاثية الطبقات

2.4.1. الهندسة العامة للتطبيق

نرغب في إنشاء تطبيق ثلاثي الطبقات له الهيكل التالي:

  • ستصبح الطبقات الثلاث مستقلة بفضل استخدام واجهات Java
  • سيتم تحقيق تكامل الطبقات الثلاث بواسطة Spring
  • سيتم إنشاء حزم منفصلة لكل طبقة من الطبقات الثلاث والتي ستسمى Control و Domain و Dao. وستحتوي حزمة إضافية على تطبيقات الاختبار.

قد تكون بنية التطبيق في Eclipse كما يلي:

Image

2.4.2. طبقة DAO للوصول إلى البيانات

ستقوم الطبقة DAO بتنفيذ الواجهة التالية:

package istia.st.demo.dao;

public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}
  • اكتب فئتين Dao1Impl1 و Dao1Impl2 اللتين تنفذان الواجهة IDao1. ستُرجع الطريقة Dao1Impl1. doSomethingInDaoLayer القيمة a+b، وستُرجع الطريقة Dao1Impl2. doSomethingInDaoLayer القيمة a-b.
  • اكتب فئة اختبار JUnit لاختبار الفئتين السابقتين

2.4.3. طبقة الأعمال

ستقوم طبقة الأعمال بتنفيذ الواجهة التالية:

package istia.st.demo.domain;

public interface IDomain1 {
  public int doSomethingInDomainLayer(int a, int b);
}
  • اكتب فئتين Domain1Impl1 و Domain1Impl2 اللتين تنفذان الواجهة IDomain1. ستحتوي هاتان الفئتان على منشئ يستقبل معلمة من النوع IDao1. ستقوم الطريقة Domain1Impl1.doSomethingInDomainLayer بزيادة a و b بمقدار وحدة واحدة ثم تمرر هذين المعلمتين إلى الطريقة doSomethingInDaoLayer للكائن من النوع IDao1 المستلم. أما الطريقة Domain1Impl2.doSomethingInDomainLayer، فستقلل a و b بمقدار وحدة واحدة قبل القيام بنفس الشيء.
  • كتابة فئة اختبار JUnit لاختبار الفئتين السابقتين

2.4.4. طبقة واجهة المستخدم

ستقوم طبقة واجهة المستخدم بتنفيذ الواجهة التالية:

package istia.st.demo.control;

public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}
  • اكتب فئتين Control1Impl1 و Control1Impl2 اللتين تنفذان الواجهة IControl1. ستحتوي هاتان الفئتان على منشئ يستقبل معلمة من النوع IDomain1. ستقوم الطريقة Control1Impl1.doSomethingInControlLayer بزيادة a و b بمقدار وحدة واحدة ثم تمرير هذين المعلمتين إلى الطريقة doSomethingInDomainLayer للكائن من النوع IDomain1 المستلم. أما الطريقة Control11Impl2.doSomethingInControlLayer، فستقلل a و b بمقدار وحدة واحدة قبل القيام بنفس الشيء.
  • كتابة فئة اختبار JUnit لاختبار الفئتين السابقتين

2.4.5. التكامل مع Spring

  • كتابة ملف تكوين Spring الذي سيحدد الفئات التي يجب أن تستخدمها كل طبقة من الطبقات الثلاث السابقة
  • اكتب فئة اختبار JUnit تستخدم تكوينات Spring مختلفة، من أجل إبراز مرونة التطبيق المكتوب
  • كتابة تطبيق مستقل (طريقة main) يعطي معلمتين للواجهة IControl1 ويعرض النتيجة التي تعرضها الواجهة.

2.4.6. حل

2.4.6.1. مشروع Eclipse

Image

تمت إضافة أرشيفات المجلد [lib] إلى [ClassPath] الخاص بالمشروع.

2.4.6.2. الحزمة [istia.st.demo.dao]

الواجهة:

1
2
3
4
5
6
7
8
9
package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}

فئة تنفيذ أولية:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public class Dao1Impl1 implements IDao1 {

     // نقوم بشيء ما في الطبقة [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a+b;
  }

}

فئة التنفيذ الثانية:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *
 */
public class Dao1Impl2 implements IDao1 {

     // نقوم بشيء ما في الطبقة [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a-b;
  }
}

2.4.6.3. الحزمة [istia.st.demo.domain]

الواجهة:

package istia.st.demo.domain;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDomain1 {

     // نقوم بشيء ما في الطبقة [domain]
  public int doSomethingInDomainLayer(int a, int b);
}

فئة تنفيذ أولى:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl1 implements IDomain1 {

     // خدمة الوصول إلى الطبقة [dao]
  private IDao1 dao1;

  public Domain1Impl1() {
     // منشئ بدون حجة
  }

     // يخزن خدمة الوصول إلى الطبقة [dao]
  public Domain1Impl1(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // يتم القيام بشيء ما في الطبقة [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a++;
    b++;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

فئة التنفيذ الثانية:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl2 implements IDomain1 {

     // خدمة الوصول إلى الطبقة [dao]
  private IDao1 dao1;

  public Domain1Impl2() {
     // منشئ بدون حجة
  }

     // يخزن خدمة الوصول إلى الطبقة [dao]
  public Domain1Impl2(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // يتم القيام بشيء ما في الطبقة [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a--;
    b--;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

2.4.6.4. الحزمة [istia.st.demo.control]

الواجهة

1
2
3
4
5
6
7
8
9
package istia.st.demo.control;

/**
 * @author ST-ISTIA
 *  
 */
public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}

فئة تنفيذ أولى:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl1 implements IControl1 {
   // فئة الأعمال في الطبقة [domain]
    private IDomain1 domain1;

  public Control1Impl1() {
     // منشئ بدون وسيطة
  }

     // تخزين خدمة الوصول إلى الطبقة [domain]
  public Control1Impl1(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // نقوم بشيء ما
  public int doSometingInControlLayer(int a, int b) {
    a++;
    b++;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

فئة التنفيذ الثانية:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl2 implements IControl1 {

     // فئة الوصول إلى الطبقة [domain]
    private IDomain1 domain1;

  public Control1Impl2() {
     // منشئ بدون حجة
  }

     // يخزن فئة الوصول إلى الطبقة [domain]
  public Control1Impl2(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // نقوم بشيء ما
  public int doSometingInControlLayer(int a, int b) {
    a--;
    b--;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

2.4.6.5. ملفات التكوين [Spring]

أول ملف [springMainTest1.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة dao -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl1">
    </bean>
     <!-- فئة الأعمال -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl1">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- فئة التحكم -->
    <bean id="control" class="istia.st.demo.control.Control1Impl1">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

ملف ثانٍ [springMainTest2.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة dao -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl2">
    </bean>
     <!-- فئة metier -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl2">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- فئة التحكم -->
    <bean id="control" class="istia.st.demo.control.Control1Impl2">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

2.4.6.6. حزمة الاختبارات [istia.st.demo.tests]

اختبار من النوع [main]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest1 {
  public static void main(String[] arguments) {
     // نسترد تنفيذ واجهة IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // نستخدم الفئة
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // نقوم بعرض النتيجة
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

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

11 mars 2005 11:25:14 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest1.xml]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl1(istia.st.demo.dao.IDao1)]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl1(istia.st.demo.domain.IDomain1)]
control(10,20)=34

اختبار آخر يستخدم ملف التكوين الثاني [Spring]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest2 {
  public static void main(String[] arguments) {
     // نسترد تنفيذ واجهة IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // نستخدم الفئة
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // نقوم بعرض النتيجة
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

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

11 mars 2005 11:28:52 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest2.xml]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl2(istia.st.demo.dao.IDao1)]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl2(istia.st.demo.domain.IDomain1)]
control(10,20)=-10

وأخيرًا، اختبار Junit:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

/**
 * @author ST-ISTIA
 *  
 */
public class JunitTest2Control1 extends TestCase {
  public void testControl1() {
     // يتم استرداد تنفيذ واجهة IControl1
    IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // نستخدم الفئة
    int a1 = 10, b1 = 20;
    int res1 = control1.doSometingInControlLayer(a1, b1);
    assertEquals(34, res1);
     // نسترد تنفيذًا آخر للواجهة IControl1
    IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // نستخدم الفئة
    int a2 = 10, b2 = 20;
    int res2 = control2.doSometingInControlLayer(a2, b2);
    assertEquals(-10, res2);
  }
}

2.5. Conclusion

يتيح إطار عمل Spring مرونة حقيقية في كل من بنية التطبيقات وتكوينها. استخدمنا مفهوم IoC، أحد ركيزتي Spring. الركيزة الأخرى هي AOP (البرمجة الموجهة نحو الجوانب) التي لم نعرضها. وهي تتيح إضافة "سلوك" إلى طريقة فئة ما عن طريق التكوين دون تعديل كودها. بشكل تخطيطي، يتيح AOP تصفية المكالمات إلى طرق معينة:

  • يمكن تنفيذ المرشح قبل أو بعد الطريقة M المستهدفة، أو كليهما.
  • تتجاهل الطريقة M وجود هذه المرشحات. يتم تعريفها في ملف تكوين Spring.
  • لا يتم تعديل كود طريقة M. المرشحات هي فئات Java يجب إنشاؤها. يوفر Spring مرشحات محددة مسبقًا، لا سيما لإدارة معاملات SGBD.
  • المرشحات هي حبوب (beans) وبالتالي يتم تعريفها في ملف تكوين Spring كحبوب.

أحد المرشحات الشائعة هو المرشح المعاملاتي. لنأخذ طريقة M من طبقة الأعمال التي تنفذ عمليتين لا تنفصلان عن البيانات (وحدة العمل). وهي تستدعي طريقتين M1 و M2 من طبقة DAO لتنفيذ هاتين العمليتين.

نظرًا لوجودها في الطبقة الوظيفية، فإن الطريقة M تتجاهل وسيط هذه البيانات. فهي لا تحتاج، على سبيل المثال، أن تفترض أن البيانات موجودة في SGBD وأنها تحتاج إلى وضع استدعاءي الطريقتين M1 و M2 ضمن معاملة SGBD. ويقع على عاتق الطبقة DAO الاهتمام بهذه التفاصيل. إذن، فإن أحد الحلول للمشكلة السابقة هو إنشاء طريقة في الطبقة DAO تستدعي هي نفسها الطريقتين M1 و M2، وهي استدعاءات ستشملها في معاملة SGBD.

حل التصفية AOP أكثر مرونة. سيسمح بتعريف مرشح يبدأ معاملة قبل استدعاء M ويقوم بعد الاستدعاء بإجراء التثبيت أو التراجع حسب الحالة.

هناك العديد من المزايا لهذا النهج:

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

بالإضافة إلى المفاهيم IoC و AOP، يقدم Spring العديد من فئات الدعم للتطبيقات ثلاثية الطبقات:

  • بالنسبة لـ Jdbc، SqlMap (IbatisHibernate، JDO (Java Data Object) في الطبقة DAO
  • لنموذج MVC في طبقة واجهة المستخدم

لمزيد من المعلومات: http://www.springframework.org.