2. المادة 1 - Spring IoC
أهداف هذا المستند:
- استكشاف إمكانيات التكوين والتكامل لإطار عمل Spring (http://www.springframework.org)
- تحديد واستخدام مفهوم IoC (انعكاس التحكم)، المعروف أيضًا باسم حقن التبعية
2.1. تكوين تطبيق ثلاثي الطبقات باستخدام Spring
لنأخذ تطبيقًا كلاسيكيًا من 3 طبقات:
سنفترض أن الوصول إلى طبقتي الأعمال و DAO يتم التحكم فيه بواسطة واجهات Java:
- واجهة [IArticlesDao] لطبقة الوصول إلى البيانات
- واجهة [IArticlesManager] لطبقة الأعمال
في طبقة الوصول إلى البيانات، أو طبقة DAO (كائن الوصول إلى البيانات)، من الشائع العمل مع نظام إدارة قواعد البيانات (DBMS) وبالتالي مع برنامج تشغيل JDBC. لننظر إلى الهيكل الأساسي لفئة تصل إلى جدول المقالات في نظام إدارة قواعد البيانات:
| public class ArticlesDaoPlainJdbc implements IArticlesDao {
// connection to data source
private String driverClassName=null;
private Connection connexion=null;
private String url = null;
private String user = null;
private String pwd = null;
....
public List getAllArticles() {
// the list of items is requested
try {
// load the JDBC driver
Class.forName(driverClassName);
// create a connection to BD
connexion = DriverManager.getConnection(url, user, pwd);
...
} catch (SQLException ex) {
...
} finally {
...
}
}
|
لإجراء عملية على نظام إدارة قواعد البيانات (DBMS)، تتطلب كل طريقة كائن [Connection] يمثل الاتصال بقاعدة البيانات، والذي سيتم من خلاله تبادل البيانات بين قاعدة البيانات ورمز Java. لإنشاء هذا الكائن، يلزم توفر أربعة عناصر من المعلومات:
| اسم فئة برنامج تشغيل JDBC لنظام إدارة قواعد البيانات |
| عنوان URL JDBC لقاعدة البيانات المراد استخدامها |
| بيانات الاعتماد المستخدمة لإنشاء الاتصال |
| كلمة المرور الخاصة بهذا الاسم المستخدم |
كيف يمكن لفئة [ArticlesDaoPlainJdbc] السابقة الحصول على هذه المعلومات؟ هناك عدة احتمالات:
الحل 1 - المعلومات مدمجة في الفئة:
| public class ArticlesDaoPlainJdbc implements IArticlesDao {
// connection to data source
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 {
// connection to data source
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 {
// a data access instance
private IArticlesDao articlesDao;
....
public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
...
// creation of a data access service
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>
<!-- data access class -->
<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؟ يمكن أن تتولى الطريقة الرئيسية للتطبيق عملية تهيئة التطبيق، إن وُجدت. أما بالنسبة لتطبيق الويب، فقد تكون هذه هي الطريقة [init] الخاصة بـ servlet الرئيسي. فكل تطبيق يحتوي على طريقة يُضمن أنها ستكون أول ما يتم تنفيذه. وعادةً ما تتم عملية إنشاء الكائنات الفردية (singletons) ضمن هذه الطريقة.
لنأخذ مثالاً. لنفترض أننا نريد اختبار فئة [ArticlesDaoPlainJdbc] السابقة باستخدام اختبار JUnit. تحتوي فئة اختبار JUnit على طريقة [setUp] يتم تنفيذها قبل أي طريقة أخرى. وهنا سنقوم بإنشاء الكائن الفردي [ArticlesDaoPlainJdbc].
إذا اتبعنا نهج تمرير معلومات التكوين عبر المنشئ، فسيكون لدينا فئة الاختبار التالية:
| public class TestArticlesPlainJdbc extends TestCase {
// tests the ArticlesDaoPlainJdbc item access class
// the data source is defined in sprintest
// an instance of the class under test
private IArticlesDao articlesDao;
protected void setUp() throws Exception{
// retrieves a data access instance
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 {
// tests the ArticlesDaoJdbc item access class
// the data source is defined in sprintest
// an instance of the class under test
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// retrieves a data access instance
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"springArticlesPlainJdbc.xml"))).getBean("articlesDao");
}
|
هنا، لا تحتاج الفئة المستدعية [TestSpringArticlesPlainJdbc] إلى معرفة المعلومات المطلوبة لتهيئة الكائن الفردي المراد إنشاؤه. بل تحتاج فقط إلى معرفة:
- [springArticlesPlainJdbc.xml]: اسم ملف تكوين Spring الموصوف أعلاه
- [articlesDao]: اسم الكائن الفردي المراد إنشاؤه
لا يؤثر أي تعديل على ملف التكوين، خارج هذين الكيانين، على كود Java. هذه الطريقة لتكوين كائنات التطبيق مرنة للغاية. لتكوين نفسه، يحتاج التطبيق إلى معرفة أمرين فقط:
- اسم ملف Spring الذي يحتوي على تعريفات الكائنات الفردية المراد إنشاؤها
- أسماء هذه الكائنات الفردية، التي يستخدمها كود Java للحصول على مرجع إلى الكائنات التي تم ربطها بها عبر ملف التكوين
2.2. حقن التبعية وانعكاس التحكم
دعونا الآن نقدم مفهوم حقن التبعية الذي يستخدمه Spring لتكوين التطبيقات. يُستخدم أيضًا مصطلح انعكاس التحكم (IoC). لننظر إلى بناء الكائن الفردي [ArticlesManagerWithDataBase] في الطبقة التجارية لتطبيقنا:
للوصول إلى البيانات من نظام إدارة قواعد البيانات (DBMS)، يجب أن تستخدم طبقة الأعمال خدمات كائن ينفذ واجهة [IArticlesDao]، على سبيل المثال، كائن من النوع [ArticlesDaoPlainJdbc]. قد يبدو كود فئة [ArticlesManagerWithDataBase] كما يلي:
public class ArticlesManagerWithDataBase implements IArticlesManager {
// a data access instance
private IArticlesDao articlesDao;
....
public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
...
// creation of a data access service
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 {
// an instance of the business class under test
private IArticlesManager articlesManager;
protected void setUp() throws Exception {
// creates an instance of the business class under test
articlesManager =
(IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
"jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
}
|
تقوم فئة الاختبار بإنشاء مثيل لفئة الأعمال [ArticlesManagerWithDataBase]، والتي تقوم بدورها بإنشاء مثيل لفئة الوصول إلى البيانات [ArticlesDaoPlainJdbc] في منشئها.
يغني حل Spring فئة الأعمال [ArticlesManagerWithDataBase] عن الحاجة إلى معرفة اسم [ArticlesDaoPlainJdbc] لفئة الوصول إلى البيانات التي تحتاجها. وهذا يسمح بتغيير الفئة دون تعديل كود Java لفئة الأعمال. يتيح Spring إنشاء كلا النوعين من العناصر الفردية في وقت واحد: واحدة لطبقة الوصول إلى البيانات والأخرى لطبقة الأعمال. سيحدد ملف تكوين Spring حبة جديدة:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<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 id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
<property name="articlesDao">
<ref bean="articlesDao"/>
</property>
</bean>
- تم تعريف الفئة التي تنفذ حبة [articlesManager]: [ArticlesManagerWithDataBase]
- يتم تعيين قيمة لحقل [articlesDao] في bean عبر العلامة <property name="articlesDao">. هذا هو الحقل المحدد في فئة [ArticlesManagerWithDataBase]:
| public class ArticlesManagerWithDataBase implements IArticlesManager {
// data access interface
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] تحتوي على منشئ. فهي لم تعد بحاجة إليه.
- القيمة التي سيخصصها Spring لحقل [articlesDao] هي قيمة 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]
- ثم سيقوم بإنشاء الكائن [articlesManager] من النوع [ArticlesManagerWithDataBase]
الآن دعونا نتخيل اختبار JUnit لفئة [ArticlesManagerWithDataBase]. قد يبدو كما يلي:
| public class TestSpringArticlesManagerWithDataBase extends TestCase {
// test business class [ArticlesManagerWithDataBase]
// an instance of the business class under test
private IArticlesManager articlesManager;
protected void setUp() throws Exception {
// retrieves a data access instance
articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
"springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
}
|
دعونا نتابع عملية إنشاء العنصرين الفريدين المحددين في ملف Spring المسمى [springArticlesManagerWithDataBase.xml].
- تطلب طريقة [setUp] أعلاه مرجعًا إلى الحبة المسماة [articlesManager]
- يستشير Spring ملف التكوين الخاص به، ويجد bean [articlesManager]. إذا كان قد تم إنشاؤه بالفعل، فإنه يعرض ببساطة مرجعًا إلى الكائن (السينجلتون)؛ وإلا، فإنه يقوم بإنشائه.
- يكتشف Spring تبعية الكائن [articlesManager] للكائن [articlesDao]. لذلك يقوم بإنشاء الكائن الفردي [articlesDao] من النوع [ArticlesDaoPlainJdbc] إذا لم يكن قد تم إنشاؤه بالفعل (ككائن فردي).
- يقوم بإنشاء الكائن الفردي [articlesManager] من النوع [ArticlesManagerWithDataBase]
يمكن تمثيل هذه الآلية بيانيًا على النحو التالي:
دعونا نستعرض الهيكل الأساسي لفئة [ArticlesManagerWithDataBase]:
| public class ArticlesManagerWithDataBase implements IArticlesManager {
// data access interface
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] دون تعديل كودها. كل ما نحتاج إلى فعله هو تغيير اسم الفئة في تعريف حبة [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;
// person display
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;
}
}
|
تحتوي الفئة على:
- حقلين خاصين، الاسم والعمر
- طرق getter و setter لهذين الحقلين
- طريقة toString لاسترداد قيمة كائن [Person] كسلسلة
- طريقة init التي سيتم استدعاؤها بواسطة Spring عند إنشاء الكائن، وطريقة close التي سيتم استدعاؤها عند تدمير الكائن
لإنشاء كائنات من النوع [Person]، سنستخدم ملف 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.
- وهو يحدد حبتين بمفاتيح "person1" و"person2" من النوع [Person]
- يقوم بتهيئة الحقول [name, 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 {
// bean factory
private ListableBeanFactory bf;
// init tests
public void setUp() {
bf = new XmlBeanFactory(new ClassPathResource("config.xml"));
}
public void test1() {
// retrieve [Person] bean keys from the Spring file
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] للتطبيق، أي في أحد الدلائل التي تبحث فيها آلة Java الافتراضية عندما تبحث عن فئة يشير إليها التطبيق. يُستخدم كائن [ClassPathResource] للبحث عن مورد في [ClassPath] للتطبيق، وهو في هذه الحالة ملف [config.xml].
- يمكن لـ Spring استخدام ملفات التكوين بتنسيقات مختلفة. يُستخدم الكائن [XmlBeanFactory] لتحليل ملف تكوين بتنسيق XML.
- تؤدي معالجة ملف Spring إلى إرجاع كائن من النوع [ListableBeanFactory]، وهو هنا الكائن bf. باستخدام هذا الكائن، يتم الحصول على bean المحدد بالمفتاح C عبر bf.getBean(C).
- تسترد الطريقة [test1] قيم الحبوب ذات المفاتيح "person1" و "person2" وتعرضها.
فيما يلي هيكل مشروع Eclipse لتطبيقنا:

تعليقات:
- يحتوي المجلد [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");
أدى ذلك إلى إنشاء كائن [person1]. يمكننا الاطلاع على سجل Spring المتعلق بذلك. ونظرًا لأننا كتبنا [init-method="init"] في تعريف كائن [person1]، فقد تم تنفيذ طريقة [init] الخاصة بكائن [Person] الذي تم إنشاؤه. ويتم عرض الرسالة المقابلة.
System.out.println("personne1=" + personne1.toString());
عرضت قيمة الكائن [Person] الذي تم إنشاؤه.
- تحدث نفس الظاهرة مع حبة المفتاح [person2].
- العملية الأخيرة
personne2 = (Personne) bf.getBean("personne2");
System.out.println("personne2=" + personne2.toString());
لم يؤد ذلك إلى إنشاء كائن جديد من النوع [Person]. لو كان الأمر كذلك، لتم عرض الطريقة [init]، وهو ما لم يحدث هنا. هذا هو مبدأ singleton. بشكل افتراضي، يقوم Spring بإنشاء مثيل واحد فقط من beans في ملف التكوين الخاص به. إنها خدمة مرجعية للكائنات. إذا طُلبت مرجعية لكائن لم يتم إنشاؤه بعد، فإنه يقوم بإنشائه وإرجاع مرجعية. إذا كان الكائن قد تم إنشاؤه بالفعل، فإن Spring يقوم ببساطة بإرجاع مرجعية إليه.
- لاحظ أنه لا يوجد أثر لطريقة [close] للكائن [Person]، على الرغم من أننا كتبنا [destroy-method=close] في تعريف bean. من الممكن أن يتم تنفيذ هذه الطريقة فقط عندما يتم استرداد الذاكرة التي يشغلها الكائن بواسطة أداة جمع القمامة. بحلول الوقت الذي يحدث فيه ذلك، يكون التطبيق قد انتهى بالفعل، ولا يكون للكتابة على الشاشة أي تأثير. يتعين التحقق من ذلك.
الآن بعد أن غطينا أساسيات تكوين Spring، سنتمكن من المضي قدمًا في شرحنا بسرعة أكبر قليلاً.
2.3.2. المثال 2
لننظر إلى فئة [Car] الجديدة التالية:
| package istia.st.springioc.domain;
public class Voiture {
private String marque;
private String type;
private Personne propriétaire;
// manufacturers
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 + "]";
}
// getters-setters
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;
}
// init-close
public void init() {
System.out.println("init voiture [" + this.toString() + "]");
}
public void close() {
System.out.println("destroy voiture [" + this.toString() + "]");
}
}
|
تحتوي الفئة على:
- ثلاثة حقول خاصة: type و make و owner. يمكن تهيئة هذه الحقول وقراءتها باستخدام طرق get و set العامة. كما يمكن تهيئتها باستخدام منشئ Car(String, String, Person). تحتوي الفئة أيضًا على منشئ بدون حجج ليتوافق مع معيار JavaBean.
- طريقة toString لاسترداد قيمة كائن [Car] كسلسلة
- طريقة init التي سيتم استدعاؤها بواسطة Spring فور إنشاء الكائن، وطريقة close التي سيتم استدعاؤها عند تدمير الكائن
لإنشاء كائنات من النوع [Car]، سنستخدم ملف 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) بالمفتاح "car1" من النوع [Car] إلى التعريفات السابقة. لتهيئة هذا المكون، كان بإمكاننا كتابة:
| <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>
|
بدلاً من اختيار الطريقة التي تم عرضها سابقًا، اخترنا هنا استخدام منشئ الفئة Car(String, String, Person). بالإضافة إلى ذلك، يحدد bean [car1] الطريقة التي سيتم استدعاؤها أثناء الإنشاء الأولي للكائن [init-method] والطريقة التي سيتم استدعاؤها أثناء تدمير الكائن [destroy-method].
لإجراء اختباراتنا، سنستخدم فئة اختبار JUnit التي تم عرضها مسبقًا، مع إضافة الطريقة [test2] التالية إليها:
| public void test2() {
// recovery of bean [voiture1]
Voiture Voiture1 = (Voiture) bf.getBean("voiture1");
System.out.println("Voiture1=" + Voiture1.toString());
}
|
تسترد الطريقة [test2] حبة [car1] وتعرضها.
تظل بنية مشروع 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]]
|
تعليقات:
- تطلب طريقة [test2] مرجعًا إلى حبة [car1]
- السطر 4: يبدأ Spring في إنشاء حبة [car1] لأن هذه الحبة لم يتم إنشاؤها بعد (singleton)
- السطر 6: نظرًا لأن bean [car1] يشير إلى bean [person2]، يتم إنشاء bean الأخير بدوره
- السطر 7: تم إنشاء bean [person2]. ثم يتم تنفيذ طريقة [init] الخاصة به.
- السطر 9: يشير Spring إلى أنه سيستخدم منشئًا لإنشاء bean [car1]
- السطر 10: تم إنشاء bean [car1]. ثم يتم تنفيذ طريقة [init] الخاصة به.
- السطر 11: تعرض طريقة [test2] قيمة الكائن [car1]
2.3.3. المثال 3
نقدم الفئة الجديدة التالية [PersonGroup]:
| package istia.st.springioc.domain;
import java.util.Map;
public class GroupePersonnes {
private Personne[] membres;
private Map groupesDeTravail;
// getters - setters
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;
}
// display
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() + "]");
}
}
|
عضواها الخاصان هما:
members: مصفوفة من الأشخاص الأعضاء في المجموعة
مجموعات العمل: قاموس يربط كل شخص بمجموعة عمل
لاحظ هنا أن فئة [PeopleGroup] لا تحدد منشئًا بدون وسيطة ليتوافق مع معيار JavaBean. تذكر أنه في حالة عدم وجود أي منشئ، يوجد منشئ "افتراضي"، وهو المنشئ بدون وسيطة الذي لا يقوم بأي شيء.
الهدف هنا هو توضيح كيف تسمح Spring بتهيئة الكائنات المعقدة، مثل تلك التي تحتوي على حقول مصفوفة أو قاموس. نضيف bean جديد إلى ملف 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>
|
- تسمح لك علامة <list> بتهيئة حقل من نوع المصفوفة أو حقل ينفذ واجهة List بقيم مختلفة.
- تسمح لك العلامة <map> بالقيام بنفس الشيء مع حقل ينفذ واجهة Map
في اختباراتنا، سنستخدم فئة اختبار JUnit التي تم عرضها سابقًا، مع إضافة الطريقة [test3] التالية إليها:
| public void test3() {
// bean retrieval [group1]]
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] مرجعًا إلى حبة [group1]
- السطر 4: يبدأ Spring في إنشاء هذا الفول
- نظرًا لأن bean [group1] يشير إلى bean [person1] و [person2]، يتم إنشاء هذين bean (السطران 6 و 9) ويتم تنفيذ طرق init الخاصة بهما (السطران 7 و 10)
- السطر 11: تم إنشاء bean [group1]. يتم الآن تنفيذ طريقة [init] الخاصة به.
- السطر 12: العرض المطلوب بواسطة طريقة [test3].
2.4. Spring لتكوين تطبيقات الويب ثلاثية الطبقات
2.4.1. البنية العامة للتطبيق
نريد إنشاء تطبيق ثلاثي المستويات بالهيكل التالي:
- ستصبح الطبقات الثلاث مستقلة عن بعضها البعض من خلال استخدام واجهات Java
- سيتولى Spring عملية تكامل الطبقات الثلاث
- سننشئ حزمًا منفصلة لكل طبقة من الطبقات الثلاث، والتي سنسميها Control و Domain و Dao. وستحتوي حزمة إضافية على تطبيقات الاختبار.
قد تكون بنية التطبيق في Eclipse كما يلي:

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 المستلم. من ناحية أخرى، ستقوم طريقة Control1Impl2.doSomethingInControlLayer بخفض a و b بمقدار واحد قبل القيام بنفس الشيء.
- اكتب فئة اختبار JUnit لاختبار الفئتين السابقتين
2.4.5. التكامل مع Spring
- اكتب ملف تكوين Spring يحدد الفئات التي يجب أن تستخدمها كل طبقة من الطبقات الثلاث السابقة
- اكتب فئة اختبار JUnit باستخدام تكوينات Spring مختلفة لإبراز مرونة التطبيق
- اكتب تطبيقًا مستقلًا (طريقة main) يمرر معلمتين إلى واجهة IControl1 ويعرض النتيجة التي ترجعها الواجهة.
2.4.6. الحل
2.4.6.1. مشروع Eclipse

تمت إضافة الأرشيفات الموجودة في المجلد [lib] إلى [ClassPath] الخاص بالمشروع.
2.4.6.2. حزمة [istia.st.demo.dao]
الواجهة:
| 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 {
// we do something in the [dao] layer
public int doSometingInDaoLayer(int a, int b) {
return a+b;
}
}
|
فئة تنفيذ ثانية:
| package istia.st.demo.dao;
/**
* @author ST-ISTIA
*
*/
public class Dao1Impl2 implements IDao1 {
// we do something in the [dao] layer
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 {
// we do something in the [domain] layer
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 {
// the [dao] layer access service
private IDao1 dao1;
public Domain1Impl1() {
// constructor with no arguments
}
// memorizes the [dao] layer access service
public Domain1Impl1(IDao1 dao1) {
this.dao1 = dao1;
}
// we do something in the [domain] layer
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 {
// the [dao] layer access service
private IDao1 dao1;
public Domain1Impl2() {
// constructor with no arguments
}
// memorizes the [dao] layer access service
public Domain1Impl2(IDao1 dao1) {
this.dao1 = dao1;
}
// we do something in the [domain] layer
public int doSomethingInDomainLayer(int a, int b) {
a--;
b--;
return dao1.doSometingInDaoLayer(a, b);
}
}
|
2.4.6.4. الحزمة [istia.st.demo.control]
الواجهة
| 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 {
// business class in layer [domain]
private IDomain1 domain1;
public Control1Impl1() {
// constructor with no arguments
}
// domain] layer access service enhancement
public Control1Impl1(IDomain1 domain1) {
this.domain1 = domain1;
}
// we're doing something
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 {
// the [domain] layer access class
private IDomain1 domain1;
public Control1Impl2() {
// constructor with no arguments
}
// stores the [domain] layer access class
public Control1Impl2(IDomain1 domain1) {
this.domain1 = domain1;
}
// we're doing something
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>
<!-- the dao class -->
<bean id="dao" class="istia.st.demo.dao.Dao1Impl1">
</bean>
<!-- the trade class -->
<bean id="domain" class="istia.st.demo.domain.Domain1Impl1">
<constructor-arg index="0">
<ref bean="dao"/>
</constructor-arg>
</bean>
<!-- the control class -->
<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>
<!-- the dao class -->
<bean id="dao" class="istia.st.demo.dao.Dao1Impl2">
</bean>
<!-- the trade class -->
<bean id="domain" class="istia.st.demo.domain.Domain1Impl2">
<constructor-arg index="0">
<ref bean="dao"/>
</constructor-arg>
</bean>
<!-- the control class -->
<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]
اختبار [رئيسي]:
| 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) {
// we retrieve an implementation of the IControl1 interface
IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
"springMainTest1.xml"))).getBean("control");
// we use the
int a = 10, b = 20;
int res = control.doSometingInControlLayer(a, b);
// the result is displayed
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) {
// we retrieve an implementation of the IControl1 interface
IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
"springMainTest2.xml"))).getBean("control");
// we use the
int a = 10, b = 20;
int res = control.doSometingInControlLayer(a, b);
// the result is displayed
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() {
// we retrieve an implementation of the IControl1 interface
IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
"springMainTest1.xml"))).getBean("control");
// we use the
int a1 = 10, b1 = 20;
int res1 = control1.doSometingInControlLayer(a1, b1);
assertEquals(34, res1);
// we retrieve another implementation of the IControl1 interface
IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
"springMainTest2.xml"))).getBean("control");
// we use the
int a2 = 10, b2 = 20;
int res2 = control2.doSometingInControlLayer(a2, b2);
assertEquals(-10, res2);
}
}
|
2.5. الخلاصة
يوفر إطار عمل Spring مرونة حقيقية في كل من بنية التطبيق وتكوينه. استخدمنا مفهوم IoC، وهو أحد ركيزتي Spring. الركيزة الأخرى هي AOP (البرمجة الموجهة نحو الجوانب)، والتي لم نتطرق إليها. تتيح لك إضافة "سلوك" إلى طريقة فئة من خلال التكوين دون تعديل كود الطريقة. بعبارات بسيطة، تتيح لك AOP تصفية المكالمات إلى طرق معينة:
- يمكن تنفيذ المرشح قبل أو بعد الطريقة المستهدفة M، أو كليهما.
- لا تعرف الطريقة M بوجود هذه المرشحات. يتم تعريفها في ملف تكوين Spring.
- لا يتم تعديل كود الطريقة M. المرشحات هي فئات Java يجب تنفيذها. يوفر Spring مرشحات محددة مسبقًا، خاصة لإدارة معاملات أنظمة إدارة قواعد البيانات (DBMS).
- المرشحات هي حبوب (beans)، وبالتالي يتم تعريفها في ملف تكوين Spring كحبوب.
أحد المرشحات الشائعة هو مرشح المعاملات. لنفترض وجود طريقة M في طبقة الأعمال تقوم بعملية لا تنفصلين على البيانات (وحدة عمل). تستدعي هذه الطريقة طريقتين، M1 و M2، في طبقة DAO لتنفيذ هاتين العمليتين.
ونظرًا لوجودها في طبقة الأعمال، فإن الطريقة M تستبعد تخزين البيانات الأساسي. على سبيل المثال، لا تحتاج إلى افتراض أن البيانات مخزنة في نظام إدارة قواعد البيانات (DBMS) أو أنها يجب أن تضم استدعاءات الطرق M1 و M2 ضمن معاملة نظام إدارة قواعد البيانات (DBMS). الأمر متروك لطبقة DAO للتعامل مع هذه التفاصيل. أحد الحلول للمشكلة السابقة هو إنشاء طريقة في طبقة DAO تستدعي بنفسها الطريقتين M1 و M2، وتضم هذه الاستدعاءات ضمن معاملة نظام إدارة قواعد البيانات (DBMS).
يعد حل التصفية AOP أكثر مرونة. فهو يتيح لك تعريف مرشح يقوم، قبل استدعاء M، ببدء معاملة، وبعد الاستدعاء، يقوم بإجراء التثبيت أو التراجع حسب الاقتضاء.
هناك عدة مزايا لهذا النهج:
- بمجرد تعريف المرشح، يمكن تطبيقه على طرق متعددة، على سبيل المثال، جميع الطرق التي تتطلب معاملة
- لا تحتاج الطرق التي تمت تصفيتها إلى إعادة كتابة
- نظرًا لأن المرشحات التي سيتم استخدامها يتم تعريفها عبر التكوين، فيمكن تغييرها
بالإضافة إلى مفاهيم IoC و AOP، يوفر Spring العديد من فئات الدعم للتطبيقات ثلاثية الطبقات:
- لـ JDBC و SQLMap (iBatis) و Hibernate و JDO (Java Data Object) في طبقة DAO
- لنموذج MVC في طبقة واجهة المستخدم
لمزيد من المعلومات: http://www.springframework.org.