寫在前面
用過Spring的朋友應該都會知道IOC這個概念,但是不見得能理解其中的原理,于是很多人使用中就會遇到對象的生命周期把控不了,對象不注入等等的問題。本篇就要實現IOC容器,并簡單實現一下MVC容器。對于Spring的其他功能,AOP、REST等暫不討論。
讀這篇文章之前,你至少使用過IOC框架,比如 Spring,知道怎么創建一個 bean,怎么給字段注入對象。有一定的Java反射基礎,因為這里的源碼是使用Java反射實現的。知道Java注解,因為需要注入的變量是用Java注解標注的。
IOC是什么
IOC ( Inverts Of Control ) 中文名叫控制反轉。舉例說明就是如果你有一個bean,在使用這個 bean 的時候你不需要使用new創建這個bean的對象,而是交給IOC容器幫你創建對象,并把對象傳遞給你的變量。使用案例如下:
class Example{
@Autowired
private final Bird bird;
public void doFly(){
bird.fly();
}
}
注意,這里的bird只聲明,并沒有初始化對象。因為IOC容器會幫你初始化對象,如果你在這里手工創建了對象,就會出現對象的生命周期混亂的問題。
再看Bird類的定義:
@Bean
class Bird{
public void fly(){
System.out.println("I am flying!");
}
}
使用@Bean注解標注這個類的對象是別的類要注入的對象,也就是Bird類的對象可以注入到Example類的bird字段里面。
怎么實現
通過分析上面的使用案例,我們大概需要做以下東西:
- 創建@Autowired注解,@Bean注解
- 掃描類
- 創建對象,注入對象
- IOC容器啟動
創建一個@Autowired注解
注解是JDK1.5之后版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數等的前面,用來對這些元素進行說明,注釋。
直接上代碼,注解的相關事宜這里不多闡述。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
掃描類
對一個IOC容器來說,并不確定要往哪個類里面的哪個變量里面注入哪個對象,唯一的依賴的標識就是上面創建的@Autowired注解,和@Bean注解。使用場景是在多個類,多個對象之下的,所以我們需要掃描到所有標注@Bean注解或者有字段標注@Autowired注解的類。
如何通過掃描拿到項目中的所有類,這部分代碼太長,也有很多解決方案,這里就不貼了,與本篇的內容偏差太大。
掃描的結果是拿到所有標注@Bean注解或者有字段標注@Autowired注解的類。
創建對象,注入對象
先分析一下正常創建對象的方法:
private final Bird bird = new Bird();
使用new關鍵字調用Bird類的構造方法,返回Bird類的對象給等號左邊的bird變量。
通過上面的幾個步驟我們拿到了所有標注@Bean注解或者有字段標注@Autowired注解的類,然后我們就需要遍歷這些類,創建對象,注入對象,實現如下:
// IOC容器所有的實例
private Map<Class<?>, Object> beanInstances=new HashMap<>();
public void init(){
// 所有被@Bean標注或者字段被@Autowired標注的類
List<Class<?>> beanClasses=searchAllClasses();
// 創建對象單例模式
for (Class<?> beanClass : beanClasses) {
Object instance=beanClass.newInstance();
beanInstances.put(beanClass, instance);
}
// 把對象注入標記@Autowired的字段
for (Class<?> autowiredClass : beanClasses) {
Field[] fields=autowiredClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
Class<?> fieldType=field.getType();
Object value=beans.get(fieldType);
field.setAccessible(true);
Object toWiredBean=beans.get(autowiredClass);
field.set(toWiredBean, value);
}
}
}
}
IOC容器啟動
上面創建的所有對象都是儲存在私有字段beanInstances的一個HashMap里面,這個HashMap里面所有的對象的@Autowired都是已經被初始化過的了,這里沒問題。當然前提是IOC容器已經被初始化過了。
但是對于程序的入口來說,比如你創建一個static void main(String[] arges)方法,你不能使用new關鍵字創建被IOC容器管理的類,因為你用new創建的對象跟IOC容器維護的對象完全是兩個不同的對象。在這種情況下你想要獲得IOC容器維護的對象,可以對外開放這個HashMap或者使用靜態的字段,讓IOC容器注入一個可以供static方法訪問的對象來實現。
Web容器方面,Servlet的創建與銷毀是Web容器去維護的。那么假設你在IndexServlet里面讓IOC容器注入了一個IndexService的對象,當你運行Web容器的時候,這個IndexService的對象肯定是為空的,原因跟上面的static main(String[] args)的問題差不多,IOC容器維護的是一套對象,Web容器創建的是另外的對象,而Servlet的對象創建不會交給IOC容器的,那么這種情況下怎么做呢?
可選的做法是創建一個代理Servlet,這個代理Servlet獲取請求的URL信息,然后根據URL信息轉發到不同的Servlet或者方法,這又是做了類似MVC框架的東西,這里不多說,只要理解了IOC
容器的原理,做一套簡單的MVC并沒有什么復雜的地方。最后會貼上我的做法。
Web容器的IOC容器初始化,可以選用Listener實現。因為Listener是在項目啟動的時候就開始加載,在Servlet創建之前。
context-param => Listener => Filter => Servlet
JunitTest的實現方法跟main方法差不多,這里不多闡述。
與本篇關系不大的簡單MVC實現
創建@Mapping注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Mapping {
public String path() default "";
public enum METHOD {GET,POST}
METHOD method() default METHOD.GET;
}
在掃描類加入對@Mapping注解的掃描
private Map<String, Object> servletInstances=new HashMap<>();
Method[] methods=autowiredClass.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Mapping.class)) {
Annotation annotation=method.getAnnotation(Mapping.class);
Method annoMethod=annotation.getClass().getDeclaredMethod("path",new Class<?>[0]);
String mappingUrl= (String) annoMethod.invoke(annotation, new Object[0]);
servletInstances.put(mappingUrl, method);
}
}
代理Servlet
String url=request.getPathInfo();
Method method=IocContainer.getMethod(url);
Object instance=IocContainer.getInstance(method.getDeclaringClass());
method.invoke(instance, req,resp);
處理請求的方法
@Mapping(path="/index.html")
public void index(HttpServletRequest request,HttpServletResponse response){
}
PS
自己實現的這套IOC與MVC已經在項目中實踐了,用起來確實比Spring的東西爽的多,雖然是參照Spring的實現,但是用這套自己做的東西一言不合就可以改源碼,而且真的很小很輕便。
要不要在github維護個項目?