自己實現spring核心功能 二

前言

上一篇我們講了spring的一些特點並且分析了需要實現哪些功能,已經把準備工作都做完了,這一篇我們開始實現具體功能。

容器加載過程

 我們知道,在spring中refesh()方法做了很多初始化的工作,它幾乎涵蓋了spring的核心流程

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        //刷新之前的準備工作,包括設置啓動時間,是否激活標識位,初始化屬性源(property source)配置
        prepareRefresh();
        //由子類去刷新BeanFactory(如果還沒創建則創建),並將BeanFactory返回
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        //準備BeanFactory以供ApplicationContext使用
        prepareBeanFactory(beanFactory);
        try {
            //子類可通過格式此方法來對BeanFactory進行修改
            postProcessBeanFactory(beanFactory);
            //實例化並調用所有註冊的BeanFactoryPostProcessor對象
            invokeBeanFactoryPostProcessors(beanFactory);
            //實例化並調用所有註冊的BeanPostProcessor對象
            registerBeanPostProcessors(beanFactory);
            //初始化MessageSource
            initMessageSource();
            //初始化事件廣播器
            initApplicationEventMulticaster();
            //子類覆蓋此方法在刷新過程做額外工作
            onRefresh();
            //註冊應用監聽器ApplicationListener
            registerListeners();
            //實例化所有non-lazy-init bean
            finishBeanFactoryInitialization(beanFactory);
            //刷新完成工作,包括初始化LifecycleProcessor,發佈刷新完成事件等
            finishRefresh();
        }
        catch (BeansException ex) {
            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();
            // Reset 'active' flag.
            cancelRefresh(ex);
            // Propagate exception to caller.
            throw ex;
        }
    }
}

 做的東西比較複雜,而我們實現做些基本的就好了。

 我們在CJDispatcherServlet 類的init方法中,實現如下業務邏輯,就能將spring功能給初始化了,就可以使用依賴注入了

    @Override
    public void init(ServletConfig config) {
        //加載配置

        //獲取要掃描的包地址

        //掃描要加載的類
    
        //實例化要加載的類

        //加載依賴注入,給屬性賦值
 
        //加載映射地址
     
    }

 

加載配置

 String contextConfigLocation = config.getInitParameter("contextConfigLocation");

        loadConfig(contextConfigLocation);

這裏會獲取到web.xml中init-param節點中的值

具體指向的是spring文件下的application.properties配置文件,裏面只有一行配置

 

通過配置的key名字可以知道,這是指定了需要掃描的包路徑

代表的是掃描紅框中定義的所有類

第二行代碼是創建了一個loadConfig方法,將包路徑傳進去

    void loadConfig(String contextConfigLocation) {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try {
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

黃色部分的代碼需要注意,這裏使用了一個成員變量

private Properties properties = new Properties();
在類的上半部分定義就好了,這裏的作用是獲取application.properties文件中的配置內容加載到properties變量中,供後面使用。

獲取要掃描的包地址

  

 //獲取要掃描的包地址
 String dirpath = properties.getProperty("scanner.package");

這裏使用配置中的key讀取出目錄地址

掃描要加載的類

   //掃描要加載的類
  doScanner(dirpath);

掃描類我們定義一個doScanner方法,把包目錄地址傳進去

 1     void doScanner(String dirpath) {
 2         URL url = this.getClass().getClassLoader().getResource("/" + dirpath.replaceAll("\\.", "/"));
 3         File dir = new File(url.getFile());
 4         File[] files = dir.listFiles();
 5         for (File file : files) {
 6             if (file.isDirectory()) {
 7                 doScanner(dirpath + "." + file.getName());
 8                 continue;
 9             }
10 
11             //取文件名
12             String beanName = dirpath + "." + file.getName().replaceAll(".class", "");
13             beanNames.add(beanName);
14         }
15     }

第二行代碼進行了轉義替換

本方法內的代碼作業是讀取指定路徑下的文件,如果是文件夾,則遞歸調用,如果是文件,把文件名稱和路徑存進集合容器內

 需要注意黃色部分的變量,是在外部定義了一個成員變量

private List<String> beanNames = new ArrayList<>();

 我們在類的上半部分加上它。

 得到的beanName如下

從這裏看出,它已經把我們定義的註解給找出來了。

 

 

實例化要加載的類

 //實例化要加載的類
   doInstance();

剛纔我們已經得到了這些定義好的類的名稱列表,現在我們需要一個個實例化,並且保存在ioc容器當中。

先定義個裝載類的容器,使用HashMap就能做到,將它設爲成員變量,在類的上半部分定義

private Map<String, Object> ioc = new HashMap<>();
接着創建一個方法doInstance
 1  void doInstance() {
 2         if (beanNames.isEmpty()) {
 3             return;
 4         }
 5         for (String beanName : beanNames) {
 6             try {
 7                 Class cls = Class.forName(beanName);
 8                 if (cls.isAnnotationPresent(JCController.class)) {
 9                     //使用反射實例化對象
10                     Object instance = cls.newInstance();
11                     //默認類名首字母小寫
12                     beanName = firstLowerCase(cls.getSimpleName());
13                     //寫入ioc容器
14                     ioc.put(beanName, instance);
15 
16 
17                 } else if (cls.isAnnotationPresent(JCService.class)) {
18                     Object instance = cls.newInstance();
19                     JCService jcService = (JCService) cls.getAnnotation(JCService.class);
20 
21                     String alisName = jcService.value();
22                     if (null == alisName || alisName.trim().length() == 0) {
23                         beanName = cls.getSimpleName();
24                     } else {
25                         beanName = alisName;
26                     }
27                     beanName = firstLowerCase(beanName);
28                     ioc.put(beanName, instance);
29                     //如果是接口,自動注入它的實現類
30                     Class<?>[] interfaces = cls.getInterfaces();
31                     for (Class<?> c :
32                             interfaces) {
33                         ioc.put(firstLowerCase(c.getSimpleName()), instance);
34                     }
35                 } else {
36                     continue;
37                 }
38             } catch (ClassNotFoundException e) {
39                 e.printStackTrace();
40             } catch (IllegalAccessException e) {
41                 e.printStackTrace();
42             } catch (InstantiationException e) {
43                 e.printStackTrace();
44             }
45         }
46     }

只要提供類的完全限定名,通過Class.forName靜態方法,我們就能將類信息加載到內存中並且返回Class 對象,通過反射來實例化,見第10行代碼,

我們通過循環beanNames集合,來實例化每個類,並將實例化後的對象裝入HashMap中

注意:第12行將類名的首字母小寫後存入map,該方法定義如下

1   String firstLowerCase(String str) {
2         char[] chars = str.toCharArray();
3         chars[0] += 32;
4         return String.valueOf(chars);
5     }

這行代碼會將字符串轉成char數組,然後將數組中第一個字符轉爲大寫,這裏採用了一種比較巧妙的方式實現,tom老師採用了一種比較騷的操作

實例化完成後,ioc容器中的數據如下:

說明:

圖片中可以看出,hashMap的key 都是小寫,value已經是對象了 ,見紅框。

這裏爲什麼要把藍框標記出來,是因爲這是類中的字段屬性,此時可以看到,雖然類已經被實例化了,可是屬性還是null呢

我這裏爲了測試依賴注入,所以加了2個接口和2個實現類

接口定義如下:
public interface IHomeService {
    String sayHi();
    String getName(Integer id,String no);
    String getRequestBody(Integer id, String no, GetUserInfo userInfo);
}



public interface IStudentService {
     String sayHi();
}
View Code

 

實現類:
@JCService
public class StudentService  implements IStudentService{
    @Override
    public String sayHi(){
        return "Hello world!";
    }
}
View Code
@JCService
public class HomeService  implements IHomeService{

    @JCAutoWrited
     StudentService studentService;
    @Override
    public String sayHi() {
      return   studentService.sayHi();
    }

    @Override
    public String getName(Integer id,String no) {
        return "SB0000"+id;
    }

    @Override
    public String getRequestBody(Integer id, String no, GetUserInfo userInfo) {
        return "userName="+userInfo.getName()+" no="+no;
    }
}
View Code

依賴實體:

public class GetUserInfo {
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

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

    public BigDecimal getGrowthValue() {
        return growthValue;
    }

    public void setGrowthValue(BigDecimal growthValue) {
        this.growthValue = growthValue;
    }

    private String name;
    private Integer age;
    private BigDecimal growthValue;

}
View Code

 



加載依賴注入,給屬性賦值

//加載依賴注入,給屬性賦值
        doAutoWrited();

  現在我們實現依賴注入,需要定義一個無參的方法doAutoWrite

 

 1     void doAutoWrited() {
 2         for (Map.Entry<String, Object> obj : ioc.entrySet()) {
 3             try {
 4                 for (Field field : obj.getValue().getClass().getDeclaredFields()) {
 5                     if (!field.isAnnotationPresent(JCAutoWrited.class)) {
 6                         continue;
 7                     }
 8                     JCAutoWrited autoWrited = field.getAnnotation(JCAutoWrited.class);
 9                     String beanName = autoWrited.value();
10                     if ("".equals(beanName)) {
11                         beanName = field.getType().getSimpleName();
12                     }
13 
14                     field.setAccessible(true);
15 
16                     field.set(obj.getValue(), ioc.get(firstLowerCase(beanName)));
17                 }
18             } catch (IllegalAccessException e) {
19                 e.printStackTrace();
20             }
21 
22         }
23 
24 
25     }

 這個方法是通過循環ioc裏面的實體,反射找出字段,看看是否有需要注入的標記JCAutoWrited,如果加了標記,就反射給字段賦值,類型從ioc容器中獲取

 

 加載映射地址 

    //加載映射地址
   doRequestMapping();

 

 映射地址的作用是根據請求的url匹配method方法

 1     void doRequestMapping() {
 2         if (ioc.isEmpty()) {
 3             return;
 4         }
 5         for (Map.Entry<String, Object> obj : ioc.entrySet()) {
 6             if (!obj.getValue().getClass().isAnnotationPresent(JCController.class)) {
 7                 continue;
 8             }
 9             Method[] methods = obj.getValue().getClass().getMethods();
10             for (Method method : methods) {
11                 if (!method.isAnnotationPresent(JCRequestMapping.class)) {
12                     continue;
13                 }
14                 String baseUrl = "";
15                 if (obj.getValue().getClass().isAnnotationPresent(JCRequestMapping.class)) {
16                     baseUrl = obj.getValue().getClass().getAnnotation(JCRequestMapping.class).value();
17                 }
18                 JCRequestMapping jcRequestMapping = method.getAnnotation(JCRequestMapping.class);
19                 if ("".equals(jcRequestMapping.value())) {
20                     continue;
21                 }
22                 String url = (baseUrl + "/" + jcRequestMapping.value()).replaceAll("/+", "/");
23                 urlMapping.put(url, method);
24                 System.out.println(url);
25             }
26         }
27     }

這裏其實就是根據對象反射獲取到JCRequestMapping上面的value值

@JCRequestMapping("/sayHi")

 取到的就是/sayHi

另外注意的是:黃色部分使用的變量是一個hashMap,在類上半部分定義的

private Map<String, Method> urlMapping = new HashMap<>();

這裏面存的是 url 和對應的method對象。後面處理請求的時候要使用到的。


結尾

容器的初始化到這裏就結束了,一共使用了4個容器來存放相關對象,後續servlet處理請求的時候會用到它們。

下一篇,將會繼續完善它,通過請求來驗證是否可以達到預期效果。另外會實現參數綁定,能處理各類請求並響應。

完整代碼地址

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