本篇文章面對(duì)的是有開(kāi)發(fā)經(jīng)驗(yàn)的Java developer 因?yàn)槲覀儗⒁獙?shí)現(xiàn)的Spring的IOC容器,
前些天由于工作中要開(kāi)發(fā)公司的Callback系統(tǒng),一直在研究Netty及IO模型,對(duì)于Netty這種非阻塞異步框架,非常崇拜,于是萌發(fā)一個(gè)想法,用Netty作為web容器,替換Tomcat研究性能.出于這種初衷,就開(kāi)始為SmileBoot項(xiàng)目開(kāi)始慢慢積累開(kāi)發(fā)知識(shí).本篇屬于小編SmileBoot中的一個(gè)模塊,為什么要起名Smile呢?因?yàn)樾【幨冀K認(rèn)為,我們要帶著好的心態(tài),才能學(xué)更多的東西,其實(shí)小編也是一個(gè)菜鳥(niǎo),之所以要寫(xiě)下來(lái),就是為了記憶和理解更深.因?yàn)槿绻炎约豪斫獾臇|西,能清楚的講給其他人,那么才算是真正的理解.
目錄
- 1.原理分析及設(shè)計(jì)
- 2.實(shí)現(xiàn)方案
- 2.1 拿到掃描范圍
- 2.2 更具掃描范圍加載范圍內(nèi)所有字節(jié)碼文件
- 2.3 定義自己的上下文對(duì)象接口及實(shí)現(xiàn)類
- 3.測(cè)試可用性
- 4.擴(kuò)展性
- 5.下篇預(yù)告
1.原理分析及設(shè)計(jì)
Spring的源碼,這里不跟著閱讀,直接去實(shí)現(xiàn),然后剛興趣的童鞋,可以自己在看看,原理是一樣的.
1.加載項(xiàng)目中所有的Class文件到Set集合
2.遍歷Set將標(biāo)記有IOC的組件的Class,獲取到,注冊(cè)到IOC容器,這個(gè)里面的重點(diǎn)是如何將Class里面的組件,注入進(jìn)來(lái).
@SmileComponent
在這里@SmileComponent注解是用來(lái)標(biāo)記,需要加入到IOC容器的類
@SmileBean
@SmileBean是用來(lái)標(biāo)記方法中返回值作為Bean,是將要被注冊(cè)到IOC容器的對(duì)象
@InsertBean
@InserBean是標(biāo)記,該字段是一個(gè)Bean,需要從IOC容器中獲取,然后注入到該對(duì)象中
2.實(shí)現(xiàn)方案
1.獲取所有的Class字節(jié)碼,在這中間我們有一個(gè)困難那就是如果知道,開(kāi)發(fā)者的所有字節(jié)碼呢?這個(gè)時(shí)候我們就可以用注解的形式,在啟動(dòng)類上做一個(gè)標(biāo)記,那么我們就能獲取到啟動(dòng)類的字節(jié)碼,從而獲取到將要掃描的跟目錄.
我們看下Spring是如何實(shí)現(xiàn)的吧
@SpringBootApplication
public class OtoSaasApplication {
public static void main(String[] args) {
SpringApplication.run(OtoSaasApplication.class, args);
}
}
在這段代碼中,有一個(gè)注解@SpringBootApplication ,了解Spring的開(kāi)發(fā)同事,都是知道這個(gè)注解其實(shí)包括了多個(gè)注解的,其中一個(gè)就是@ComponentScan
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
@AliasFor("basePackages")
String[] value() default {}
}
那么我們就可以知道,其實(shí)也main方法所包含的注解,拿到根目錄的.這個(gè)有一個(gè)Spring的特性,那就是如果啟動(dòng)類在最外層的,那么默認(rèn)就是掃描,其子目錄中的Class,如果不是在根目錄,那么要指定掃描的范圍.
2.1拿到掃描范圍
那么我們想,如果用戶不指定,我們?cè)趺茨玫礁夸浤?
好,如果有疑惑的話,那么久帶著疑惑,看下面這段代碼吧!
我們定一個(gè)注解@SmileBootApplication
目錄就是獲取到用戶的根目錄,這里關(guān)于注解不在解釋,如果有不了解實(shí)現(xiàn)注解的可以看小編SpringBoot實(shí)踐中的自定義注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SmileBootApplication {
String[] basePackages() default {};
}
@SmileBootApplication
public class SmileApplication {
public static void main(String[] args) {
SmileApplication.run(SmileApplication.class, args);
}
}
我們定義一個(gè)方法也就是在run方法中,根據(jù)class,文件,獲取到注解的根目錄
public static String getBaseRootPackage(Class<?> cls) {
SmileBootApplication declaredAnnotation = null;
try {
declaredAnnotation = cls.getDeclaredAnnotation(SmileBootApplication.class);
} catch (Exception e) {
throw new IllegalArgumentException("請(qǐng)?zhí)砑覢SmileBootApplication");
}
/**
* 獲取注解上的掃描目錄
* 如果沒(méi)有指定,就從當(dāng)前目錄獲取
*/
String[] strings = declaredAnnotation.basePackages();
String baseRootPackage = "";
if (strings.length == 0) {
baseRootPackage = cls.getPackage().getName();
}
return baseRootPackage;
}
看到這里,我們已經(jīng)拿到了項(xiàng)目的根目錄,或者說(shuō)是將要掃描的范圍了
2.2 獲取指定目錄下的所有字節(jié)碼文件
這個(gè)時(shí)候我們要知道一個(gè)基礎(chǔ)的方法,那就是
/**
* @param className 完整類路徑
* @param isInitialized 是否初始化 第2個(gè)boolean參數(shù)表示類是否需要初始化Class.forName(className)默認(rèn)是需要初始化吴攒。一旦初始化,就會(huì)觸發(fā)目標(biāo)對(duì)象的 static塊代碼執(zhí)行,static參數(shù)也也會(huì)被再次初始化
* @param classLoader 類加載器
* @return
*/
Class.forName(className, isInitialized, classLoader);
我們要用到一個(gè)工具類ClassUtils,該類中可以將根目錄中所有字節(jié)碼(.java,.jar文件)加載到Set<Class<?>>set中
這個(gè)工具也不是小編寫(xiě)的,是參考了很多博客大拿,發(fā)現(xiàn)都有用到,但是具體出自哪位,就不曉得了,那么也分享給大家
可以參考
2.3 定義自己的上下文對(duì)象接口及實(shí)現(xiàn)類
/**
* @Package: pig.boot.ioc.context
* @Description: 上下文
* @author: liuxin
* @date: 2017/11/17 下午11:52
*/
public interface ApplicationContext {
Object getBean(String var1);
<T> T getBean(String name, Class<T> requiredType);
<T> T getBean(Class<T> name);
boolean containsBean(String var1);
void scan(String basePackRoot);
}
public class SmileApplicationContext implements ApplicationContext {
/**
* 掃描所有的類,并裝載
*
* @param basePackRoot
*/
@Override
public void scan(String basePackRoot) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Set<Class<?>> classesByPackage = null;
try {
/**
* recursively 是否從根目錄,向下查找
*/
classesByPackage = ClassUtils.getClassesByPackageName(classLoader, basePackRoot, true);
} catch (IOException e) {
e.printStackTrace();
}
/**
* 加載到所有的bean
*/
allBeans.addAll(classesByPackage);
/**
*掃描所有的標(biāo)記,加入到容器
*/
classesByPackage.forEach(this::scanComponent);
/**
* 將沒(méi)有注冊(cè)的bean檢查,然后注入
*/
processEarlyBeans();
}
}
這個(gè)類的重點(diǎn)方法就是scan掃描所有的標(biāo)記,并從set中拿到每個(gè)字節(jié)碼,傳給scanComponent 方法去解析注冊(cè)
這個(gè)時(shí)候我們可能遇到一種情況,就是BeanA中需要注冊(cè)BeanB但是可能BeanB此時(shí)并沒(méi)有解析到,那么這個(gè)時(shí)候,就要考慮,把暫時(shí)實(shí)例化不了的,放入到delayBeans,等所有能解析的解析之后,在回過(guò)頭加載,我們來(lái)看這個(gè)方法,在看之前我們定義這樣一個(gè)類 BeanDefinition
用處就是講Bean的class和實(shí)例化對(duì)象都保存起來(lái)
/**
* @Package: pig.boot.ioc.context
* @Description: bean描述
* @author: liuxin
* @date: 2017/11/17 下午11:53
*/
public class BeanDefinition {
Class<?> clazz;
Object instance;
public BeanDefinition(Class<?> clazz, Object instance) {
this.clazz = clazz;
this.instance = instance;
}
}
/**
* 掃描所有被標(biāo)記的組件
*/
public void scanComponent(Class<?> nextCls) {
SmileComponent declaredAnnotation = nextCls.getDeclaredAnnotation(SmileComponent.class);
Object beanInstance = null;
if (declaredAnnotation != null) {
try {
beanInstance = nextCls.newInstance();
String beanName = declaredAnnotation.vlaue();
if (beanName.isEmpty()) {
beanName = nextCls.getSimpleName();
}
/**
* 保證bean名稱的唯一性
*/
Long beanId = beanIds.get();
//將類名,首字母小寫(xiě),并檢查是否存在,如果存在就后面添加id,這個(gè)id是原子操作,保證唯一.
beanName= getUniqueBeanNameByClassAndBeanId(nextCls,beanId);
/**
* 實(shí)例化里面的需要注入的字段都獲取到
* 如果返回true就可以直接添加到IOC容器
* lastChance=true 如果注入失敗就報(bào)錯(cuò),false不報(bào)錯(cuò),因?yàn)榈谝淮?可能所有類沒(méi)有初始化,所以等待延遲加載方法去,加載
*/
if (autowireFields(beanInstance, nextCls, false)) {
registeredBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
} else {
/**
* 上面那種情況,可能會(huì)出現(xiàn),當(dāng)要注入,但是被注入的未加載到IOC容器中的情況,所以對(duì)于這種,就添加到earlyBeans中,后期注入
*/
delayBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
}
/**
* 獲取方法上的bean
* 因?yàn)榉椒隙ㄊ怯蟹祷刂?的返回值就是實(shí)例化對(duì)象,所以可以直接,加入到IOC容器
*/
createBeansByMethodsOfClass(beanInstance, nextCls);
} catch (Exception e) {
}
}
}
3.測(cè)試可用性
@SmileBootApplication
public class SmileApplication {
public static void main(String[] args) {
SmileApplicationContext run = SmileApplication.run(SmileApplication.class, args);
System.out.println(run.getBean(BeanB.class).toString());
System.out.println(run.getBean(BeanA.class).beanB().toString());
//BeanB{content='hi. iam is beanB'}
//BeanB{content='hi. iam is beanB'}
}
}
/**
* @Package: pig.boot.ioc.context
* @Description: 獲取參數(shù)
* @author: liuxin
* @date: 2017/11/17 下午11:55
*/
@SmileComponent
public class BeanA {
private String content;
@InsertBean
private BeanB beanb;
public BeanA() {
}
public BeanA(String content) {
this.content = content;
}
@SmileBean
public BeanB beanB() {
return new BeanB("hi. iam is beanB");
}
}
4.可擴(kuò)展性
-
定義上下文對(duì)象接口類,developer,可以定義自己的上下文類
一個(gè)軟件實(shí)體如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開(kāi)放崇呵,對(duì)修改關(guān)閉
ApplicationContextInitialezer 初始化類執(zhí)行,獲取初始化條件和初始化方法,指定合適的上下文實(shí)現(xiàn)類
? 不要存在多于一個(gè)導(dǎo)致類變更的原因,通俗的說(shuō)陨囊,即一個(gè)類只負(fù)責(zé)一項(xiàng)職責(zé)。
- 小編最喜歡的方法是抽象,即具有共同特征的方法,用抽象類去實(shí)現(xiàn),具體的方法有繼承類去實(shí)現(xiàn)
- 接口隔離,即依賴最小的接口,eg.A接口有五個(gè)方法 B此時(shí)要用3個(gè),C要用2個(gè),但是他們不得不全部實(shí)現(xiàn).此時(shí)我們可以把A接口拆分為2個(gè). 當(dāng)D5個(gè)方法的時(shí)候,就繼承2個(gè)接口,就可以
5.下篇預(yù)告
定義@SmileGetMapping,@SmilePostMapping,注解,綁定處理邏輯handler. 放入SmileNettyTaskHandler中,交給Netty處理異步處理
附錄
好的代碼就想一本書(shū),讀的書(shū)越多,思路就越廣,想法就越多
每個(gè)開(kāi)發(fā)人員要把自己當(dāng)做一個(gè)工程師,而不是一個(gè)coding 的碼農(nóng),工程師考慮問(wèn)題要從頂層設(shè)計(jì)考慮,而不是為了單純解決一個(gè)問(wèn)題而code.