【Spring】手寫模擬spring IoC容器

自定義異常

/**
* 自定義異常
*/
public class SpringException extends RuntimeException {
   
public SpringException(String msg) {
       
super(msg);
    }
}

 

自定義註解

// 要設置註解的生命週期是運行時,如果不設置運行時,那麼代碼在運行時註解就被去掉了,spring也就掃描不到了
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
   
public String value();
}

 

一、使用XML向容器中裝配Bean

public class BeanFactory {
   
// 這裏我們簡單使用map來存儲在factory中管理的bean,在真實的spring源碼中是通過一個定義的數據結構來存儲的,因爲一個bean有很多數據需要存儲,只靠一個map是遠遠不夠的
    Map<String, Object> map = new HashMap<String, Object>();
 
   
public BeanFactory(String xml) {
        parseXml(xml);
    }
 
   
/**
     * 解析XML配置文件
     *
     * @param xml
     */
    public void parseXml(String xml) throws SpringException {
       
// 生成xml文件對象
        // 獲取xml文件路徑
        String path = this.getClass().getResource("/").getPath() + "//" + xml;
       
try {
           
// 解決路徑中文亂碼問題
            path = URLDecoder.decode(path, "UTF-8");
        }
catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        File file =
new File(path);
        SAXReader reader =
new SAXReader();
       
try {
           
// 生成xmlDOM
            Document document = reader.read(file);
           
// 獲得DOM樹上的根節點
            Element elementRoot = document.getRootElement();
 
           
// 獲取根標籤中的default-autowire屬性
            Attribute attribute = elementRoot.attribute("default-autowire");
           
// 標記是否開啓了自動裝配
            boolean flag = false;
           
// 判斷是否有default-autowire,即是否開啓自動裝配
            if (attribute != null) {
                flag = true;
            }
 
           
// 從根節點開始向下遍歷樹上的所有一級節點
            for (Iterator<Element> it = elementRoot.elementIterator(); it.hasNext(); ) {
               
/**
                 * 1、實例化對象
                 */
 
               
// 獲取一個標籤
                Element elementFirstChil = it.next();
 
               
// 獲取這個標籤的id屬性對象
                Attribute attributeId = elementFirstChil.attribute("id");
               
// 獲取id屬性的值
                String beanName = attributeId.getValue();
 
               
// 獲取這個標籤的class屬性對象
                Attribute attributeClass = elementFirstChil.attribute("class");
               
// 獲取class屬性的值
                String clazzName = attributeClass.getValue();
               
// 獲取beanclass對象
                Class clazz = Class.forName(clazzName);
 
               
// 生成對應bean的對象引用
                Object object = null;
 
 
               
/**
                 * 2、維護依賴關係
                 * 看這個對象有沒有依賴(判斷xmlbean標籤中是否有property這個子標籤/或者判斷類是否有屬性)
                 * 如果有則注入
                 */
                for (Iterator<Element> itSecond = elementFirstChil.elementIterator(); itSecond.hasNext(); ) {
                   
/**
                     * 得到refvalue,通過value得到要注入的對象(從map中取得)
                     * 得到name的值,根據這個值回去一個Filed對象(Filed對象用來取得一個類中的屬性)   這裏爲了簡單默認使用set方法來進行注入,所以要取name
                     * 通過filedset方法來注入對象
                     */
                    // 取得二級子標籤很替
                    Element elementSecondChil = itSecond.next();
                   
// 查看<bean>標籤的子標籤中是否有property標籤,有的話說明這個bean有依賴對象,需要用setter方法注入
                    if (elementSecondChil != null && "property".equals(elementSecondChil.getName())) {
                       
// 生成對應bean的對象實例
                        object = clazz.newInstance();
 
                       
// 獲取依賴對象的id
                        String refValue = elementSecondChil.attribute("ref").getValue();
                       
// map中獲取要注入的bean
                        Object injectObject = map.get(refValue);
 
                       
// 獲取<property>標籤的name屬性,表示的是要匹配的set方法名,但是這裏爲了簡單起見,就默認service對象daoset方法名與service對象中UserDao屬性名一致,都是dao
                        String nameValue = elementSecondChil.attribute("name").getValue();
                       
// 獲取該類屬性名爲nameValue的屬性的Field對象,來進行注入
                        Field field = clazz.getDeclaredField(nameValue);
                       
// 因爲這個屬性是私有的,所以要開啓暴力反射,將Accessible設置爲true
                        field.setAccessible(true);
                       
// 將要注入的對象注入到object中,這裏object表示的是被注入的對象,也就是serviceinjectObject表示的是注入的對象,也就是dao
                        field.set(object, injectObject);
 
 
                   
// 使用構造方法注入
                    } else if (elementSecondChil != null && "constructor-arg".equals(elementSecondChil.getName())){
                       
// 獲取依賴對象的id
                        String refValue = elementSecondChil.attribute("ref").getValue();
 
                       
// map中獲取要注入的bean
                        Object injectObject = map.get(refValue);
 
                       
// 獲取被注入對象的屬性名
                        Attribute nameAttribute = elementSecondChil.attribute("name");
                        String nameValue = null;
                       
if (nameAttribute != null) {
                            nameValue = nameAttribute.getValue();
                        }
 
                       
// 如果XML標籤傳入了name屬性就直接通過注入對象的Field類獲取注入對象的Class
                        if (nameValue != null) {
                           
// 獲取該類屬性名爲nameValue的屬性的Field對象
                            Field field = clazz.getDeclaredField(nameValue);
                           
// 直接通過Field對象獲取注入對象的Class
                            Class injectObjectClazz = field.getType();
                           
// 通過被注入對象的Class獲取構造方法對象,並將注入對象的Class傳入,這樣就可以用這個參數是注入對象的構造方法對象來創建注入daoservice對象
                            Constructor constructor = clazz.getConstructor(injectObjectClazz);
                           
// 將注入對象dao傳入構造方法中並且生成service的實例對象
                            object = constructor.newInstance(injectObject);
 
 
                       
// 如果XML標籤沒有傳入name屬性就通過ref屬性指定的在spring容器中的bean獲取注入對象的Class
                        } else {
                           
// 通過spring容器中的bean獲取注入對象的Class
                            Class injectObjectClazz = injectObject.getClass();
                           
// 由於在spring容器中的bean的類型是UserDaoImpl,而被注入對象中屬性的類型是接口UserDao,所以會出現兩者類型不一致,無法獲取正確的構造方法的情況(構造方法參數類型是UserDao
                            Constructor constructor = clazz.getConstructor(injectObjectClazz.getInterfaces()[0]);
                           
// 將注入對象dao傳入構造方法中並且生成service的實例對象
                            object = constructor.newInstance(injectObject);
                        }
 
                    }
                }
 
               
/**
                 * 使用自動裝配
                 * 以前不使用自動裝配,spring通過掃描XML來獲取bean之間的依賴關係,讀取完XML後會去代碼中掃描屬性,進一步確認依賴關係,如果XML定義了依賴關係,但是代碼中並沒有這種依賴關係,就會拋出異常,上面我們模擬的過程沒有寫拋出異常的部分,讀者可以自行添加
                 *
                 * byType:直接通過代碼中定義的依賴關係來進行自動裝配,如果被注入對象中有注入對象這個屬性,那麼就說明他們之間有依賴關係,通過Fidld來獲得注入對象的Class對象,再通過反射實現注入
                 */
                // 需要在掃描完上面的所有子標籤之後再判斷是不是開啓了自動裝配,因爲spring手動裝配的優先級高於自動裝配,如果上面發現bean標籤裏有子標籤已經完成了手動裝配,我們就不需要再執行後面的自動裝配邏輯了,這裏的判斷標準就是被注入對象object是否已經被實例化
                if (object == null && flag) {
                   
// 使用byType方式自動裝配
                    if ("byType".equals(attribute.getValue())) {
                       
// 判斷是否有依賴
                        // 獲取bean中的所有屬性的Field對象
                        Field fields[] = clazz.getDeclaredFields();
                       
// 遍歷所有的屬性,查找有沒有和spring容器中的bean類型一樣的
                        for (Field field : fields) {
                           
// 得到屬性類型
                            Class injectObjectClazz = field.getType();
                           
/**
                             * 由於是byType 所以需要遍歷map中的所有對象
                             * 判斷對象的類型是不是和這個injectObjectClazz一致
                             */
                            // 記錄spring容器中和注入對象類型匹配的個數
                            int count = 0;
                            Object injectObject = null;
                           
for (String key : map.keySet()) {
                               
// 獲取spring容器中bean的類型,這裏爲了我們樣例代碼演示方便就是獲取的接口
                                Class temp = map.get(key).getClass().getInterfaces()[0];
                               
if (temp.getName().equals(injectObjectClazz.getName())) {
                                    injectObject = map.get(key);
 
                                   
// 記錄類型一直個數
                                    count++;
                                }
                            }
 
                           
if (count > 1) {
                               
throw new SpringException("需要一個bean,但是在容器中存在" + count + "個符合條件的bean");
                            }
else {
                               
// 創建被注入對象
                                object = clazz.newInstance();
                               
// 因爲是私有屬性,所以使用field.set注入需要開啓暴力注入
                                field.setAccessible(true);
                               
// 實現注入
                                field.set(object, injectObject);
                            }
                        }
                    }
                }
               
// 沒有子標籤的情況需要單獨給bean實例化
                if (object == null) {
                    object = clazz.newInstance();
 
                }
 
               
// 將生成的bean實例對象存入到map
                map.put(beanName, object);
            }
        }
catch (Exception e) {
            e.printStackTrace();
        }
 
        System.out.println(map);
    }
 
   
public Object getBean(String beanName) {
       
return map.get(beanName);
    }
}

 

測試使用的三套XML配置文件

 
<?xml version="1.0" encoding="UTF-8"?>
<!--
    1、哪些類需要我來管理
    2、怎麼告訴我這些類(bean標籤)
    3、怎麼維護依賴關係(setter方法或構造方法)
    4、怎麼體現setter或者構造方法(使用propertyconstructor內嵌標籤)
 
    spring就是通過bean標籤來完成上面兩個操作的
-->
<beans>
    <!--
        下面面的XML配置文件的含義就是我們將UserDaoImplUserServiceImpl裝配到spring容器中,將他們的id分別設置成daoservice
        service的依賴dao,並且通過相應的setter方法/構造方法注入
    -->
 
 
   
<!--告訴sping容器UserDaoImpl需要它來管理-->
    <bean id="dao" class="priv.priv.cy.dao.UserDaoImpl"></bean>
 
   
<!--告訴sping容器UserServiceImpl需要它來管理-->
    <bean id="service" class="priv.priv.cy.service.UserServiceImpl">
        <!--注入dao對象的前提就是dao對象已經裝配給spring容器去管理,所以要先寫一個dao對象的bean-->
        <!--注入方法在setterconstructor之中選一個就行,不過要在代碼中把需要的構造方法或者setter方法編寫出來-->
 
       
<!--
            表示使用setter方法完成注入
            property標籤表示使用setter方法完成注意,裏面有兩個屬性,nameref
            name:表示的是setter方法的名字,name屬性的值會去匹配將setter方法中的前綴set去掉之後,再將剩下的首字母小寫得到的方法名,如果找到名字一致的注入操作就是由這個setter方法
                 因爲我們一般就是用默認生成的stter方法名,所以這個name標籤也可以不加,只是爲了防止有需要修改setter方法名時,就需要特別指定一下setter方法了
            ref:這個指定的是要將哪個bean注入給service,後面寫的是spring容器中beanid
 
            所以下面這句話的意思就是我們要將spring容器中iddaobean,通過service對象中一個名爲daostter方法將其注入給service對象
        -->
        <property name="dao" ref="dao"></property>
 
 
 
   
</bean>
</beans>

 
<?xml version="1.0" encoding="UTF-8"?>
<!--
    1、哪些類需要我來管理
    2、怎麼告訴我這些類(bean標籤)
    3、怎麼維護依賴關係(setter方法或構造方法)
    4、怎麼體現setter或者構造方法(使用propertyconstructor內嵌標籤)
 
    spring就是通過bean標籤來完成上面兩個操作的
-->
<beans>
    <!--
        下面面的XML配置文件的含義就是我們將UserDaoImplUserServiceImpl裝配到spring容器中,將他們的id分別設置成daoservice
        service的依賴dao,並且通過相應的setter方法/構造方法注入
    -->
 
 
   
<!--告訴sping容器UserDaoImpl需要它來管理-->
    <bean id="dao" class="priv.priv.cy.dao.UserDaoImpl"></bean>
 
   
<!--告訴sping容器UserServiceImpl需要它來管理-->
    <bean id="service" class="priv.priv.cy.service.UserServiceImpl">
        <!--注入dao對象的前提就是dao對象已經裝配給spring容器去管理,所以要先寫一個dao對象的bean-->
        <!--注入方法在setterconstructor之中選一個就行,不過要在代碼中把需要的構造方法或者setter方法編寫出來-->
 
       
<!--
            使用構造方法注入
            name:表示的是被注入的對象中所依賴的這個對象的屬性名,不是構造方法傳入參數的屬性名。也就是service對象中的UserDao類型的屬性名爲dao。這個name也可以不寫,spring會自動找到構造方法去進行注入
            ref:表示的是beanid,也就是將spring容器中iddao的這個bean注入到service
 
            下面這段代碼的意思就是將spring容器中iddao的這個bean通過構造方法注入到service中屬性名爲dao的這個屬性
        -->
        <constructor-arg name="dao" ref="dao"></constructor-arg>
 
   
</bean>
</beans>

 
<?xml version="1.0" encoding="UTF-8"?>
<!--
    1、哪些類需要我來管理
    2、怎麼告訴我這些類(bean標籤)
    3、怎麼維護依賴關係(setter方法或構造方法)
    4、怎麼體現setter或者構造方法(使用propertyconstructor內嵌標籤)
 
    spring就是通過bean標籤來完成上面兩個操作的
-->
<beans default-autowire="byType">
    <!--
        下法注面面的XML配置文件的含義就是我們將UserDaoImplUserServiceImpl裝配到spring容器中,將他們的id分別設置成daoservice
        並且實現自動裝配(byName/byType
    -->
 
   
<!--
        通過byType實現自動裝配
        這裏需要注意的就是byType自動裝配是通過Field類的set方法反射注入的,不依賴setter方法和構造方法,即使沒有setter方法和構造方法依舊可以成功注入
    -->
 
 
   
<!--告訴sping容器UserDaoImpl需要它來管理-->
    <bean id="dao" class="priv.priv.cy.dao.UserDaoImpl"></bean>
 
   
<!--告訴sping容器TestDaoImpl需要它來管理-->
    <bean id="test" class="priv.priv.cy.dao.TestDaoImpl"></bean>
 
   
<!--告訴sping容器UserDaoImpl1需要它來管理-->
<!--    <bean id="dao1" class="priv.priv.cy.dao.UserDaoImpl1"></bean>-->
 
   
<!--告訴sping容器UserServiceImpl需要它來管理-->
    <bean id="service" class="priv.priv.cy.service.UserServiceImpl">
        <!--手動裝配優先於自動裝配-->
<!--        <property name="dao" ref="dao"></property>-->
    </bean>
</beans>

 

二、使用註解向容器中裝配Bean

 
package org.spring.util;
 

import org.spring.annotation.Service;
 

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
 

/**
 * 實現對註解的掃描,使用註解將bean裝配到spring容器
 */
public class AnnotationConfigApplicationContext {
 
   
/**
     * 真正的AnnotationConfigApplicationContext也有scan這個方法,方法原型和作用我們下面模擬的這個方法作用是一致的。
     * @param basePackage
     */
    public void scan(String basePackage) {
       
// 獲得classpath路徑,通過這個路徑獲取類的全限定名,這樣纔可以將bean實例化
        // 下面這個是動態獲取classpath路徑,因爲項目部署到不同的服務器,它的classpath路徑是會變化的
        String rootPath = this.getClass().getResource("/").getPath();
 
       
// 將包路徑轉換成文件路徑
        // 將一個.換成\   .\是一個轉義字符
        String basePackagePath = basePackage.replaceAll("\\.", "\\\\");
 
       
// classpath路徑和傳入的包路徑拼接起來就得到了要掃描的class文件所在路徑
        String path = rootPath + "//" + basePackagePath;
       
try {
           
// 解決路徑中文亂碼問題
            path = URLDecoder.decode(path, "UTF-8");
        }
catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        File file =
new File(path);
       
// 獲取當前路徑的所有文件名  這個是通過遞歸的方法,將這個路徑下面所有的文件名都取出來,不取文件夾名
        String[] names = file.list();
       
for (String name : names) {
            name = name.replaceAll(
".class", "");
           
try {
               
// 獲取要交給容器的beanClass對象,用來實例化
               Class clazz =  Class.forName(basePackage + "." + name);
 
              
// 如果是模擬@Autowired使用byType的話,就利用反射通過clazz將他的所有的屬性field對象取出,然後通過field獲取屬性的Type,查看spring容器中有沒有符合條件的bean,有的話就取出注入,基本過程和模擬xml的一樣
 
              
// 判斷該類上的註解類型,這裏只演示@Service
                if (clazz.isAnnotationPresent(Service.class)) {
                   
// 獲取註解的對象
                    Service service = (Service) clazz.getAnnotation(Service.class);
                   
// 取得註解中的value,用來給bean設置name
                    System.out.println(service.value());
                   
// 後面就是將bean實例化放入factory中去,前面對xml方式的模擬已經有了,這裏就不再寫了,直接實例化輸出驗證
                    System.out.println(clazz.newInstance());
                }
 
            }
catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}


其他文章:【Spring】史上最全的spring IoC使用講解 
                  【Spring】面試官,請別再問Spring Bean的生命週期了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章