jsoncat: 仿 Spring Boot 但不同於 Spring Boot 的一個輕量級的 HTTP 框架
國慶節的時候,我就已經把 jsoncat 的 IoC 功能給寫了,具體可以看這篇文章《手寫“SpringBoot”近況:IoC模組已經完成》 。
今天這篇文章就來簡單分享一下自己寫 IoC 的思路與具體的程式碼實現。
IoC (Inverse of Control:控制反轉) 和 AOP(Aspect-Oriented Programming:面向切面程式設計) 可以說是 Spring 框架提供的最核心的兩個功能。但凡是瞭解過 Spring 的小夥伴,那肯定對這個兩個概念非常非常瞭解。不瞭解的小夥伴,可以檢視《面試被問了幾百遍的 IoC 和 AOP ,還在傻傻搞不清楚?》這篇通俗易懂的文章。
考慮到這篇文章要手寫 Spring 框架的 IoC 功能,所以,我這裡還是簡單介紹一下 IoC 。如果你不太清楚 IoC 這個概念,一定要搞懂之後再看後面具體的程式碼實現環節。
IoC 介紹
IoC(Inverse of Control:控制反轉)是一種設計思想,也就是 將原本在程式中手動建立物件的控制權交由Spring框架來管理。 IoC 在其他語言中也有應用,並非 Spring 特有。
IoC 容器
IoC 容器是用來實現 IoC 的載體,被管理的物件就被存放在IoC容器中。IoC 容器在 Spring 中實際上就是個Map(key,value),Map 中存放了各種被管理的物件。
IoC 解決了什麼問題
將物件之間的相互依賴關係交給 IoC 容器來管理,並由 IoC 容器完成物件的注入。這樣可以很大程度上簡化應用的開發,把應用從複雜的依賴關係中解放出來。 IoC 容器就像是一個工廠一樣,當我們需要建立一個物件的時候,只需要配置好配置檔案/註解即可,完全不用考慮物件是如何被建立出來的。 在實際專案中一個 Service 類可能有幾百甚至上千個類作為它的底層,假如我們需要例項化這個 Service,你可能要每次都要搞清這個 Service 所有底層類的建構函式,這可能會把人逼瘋。如果利用 IoC 的話,你只需要配置好,然後在需要的地方引用就行了,這大大增加了專案的可維護性且降低了開發難度。
IoC 和 DI 別再傻傻分不清楚
IoC(Inverse of Control:控制反轉)是一種設計思想 或者說是某種模式。這個設計思想就是 將原本在程式中手動建立物件的控制權,交由 Spring 框架來管理。 IoC 在其他語言中也有應用,並非 Spring 特有。IoC 容器是 Spring 用來實現 IoC 的載體, IoC 容器實際上就是個 Map(key,value),Map 中存放的是各種被管理的物件。
IoC 最常見以及最合理的實現方式叫做依賴注入(Dependency Injection,簡稱 DI)。
並且,老馬(Martin Fowler)在一篇文章中提到將 IoC 改名為 DI,原文如下,原文地址:https://martinfowler.com/articles/injection.html 。
IoC實現思路
?注意 :以下思路未涉及解決迴圈依賴的問題!
開始程式碼實現之前,我們先簡單聊聊實現 IoC 的思路,搞清楚了思路之後,實現起來就非常簡單了。
- 掃描指定包下的特定註解比如
@Component
標記的類,並將這些類儲存起來。 - 遍歷所有被特定註解比如
@Component
標記的類,然後將這些類通過反射例項化並通過一個 Map 儲存起來,Map 的 key 為類名,value為類物件。 - 再一次遍歷所有被特定註解比如
@Component
標記的類,並獲取類中所有的欄位,如果類被@Autowired
註解標記的話,就進行第 4 步。 - 通過欄位名 key,從bean容器中獲取對應的物件 value。
- 判斷獲取到的物件是否為介面。如果是介面的話,需要獲取介面對應的實現類,然後再將指定的實現類的例項化物件通過反射賦值給指定物件。如果不是介面的話,就直接將獲取到的物件通過反射賦值給指定物件。
IoC 實現核心程式碼
核心註解
@Autowired
:註解物件
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
}
@Component
:宣告物件被IoC容器管理
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
String name() default "";
}
@Qualifier
: 指定注入的bean(當介面有多個實現類的時候需要使用)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
String value() default "";
}
工具類
簡單封裝一個反射工具類。工具類包含3個後面會用到的方法:
scanAnnotatedClass()
:掃描指定包下的被指定註解標記的類(使用Reflections這個反射框架一行程式碼即可解決掃描獲取指定註解的類)。newInstance()
: 傳入 Class 即可返回 Class 對應的物件。setField()
:為物件的指定欄位賦值。
@Slf4j
public class ReflectionUtil {
/**
* scan the classes marked by the specified annotation in the specified package
*
* @param packageName specified package name
* @param annotation specified annotation
* @return the classes marked by the specified annotation in the specified package
*/
public static Set<Class<?>> scanAnnotatedClass(String packageName, Class<? extends Annotation> annotation) {
Reflections reflections = new Reflections(packageName, new TypeAnnotationsScanner());
Set<Class<?>> annotatedClass = reflections.getTypesAnnotatedWith(annotation, true);
log.info("The number of class Annotated with @RestController :[{}]", annotatedClass.size());
return annotatedClass;
}
/**
* create object instance through class
*
* @param cls target class
* @return object created by the target class
*/
public static Object newInstance(Class<?> cls) {
Object instance = null;
try {
instance = cls.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
log.error("new instance failed", e);
}
return instance;
}
/**
* set the value of a field in the object
*
* @param obj target object
* @param field target field
* @param value the value assigned to the field
*/
public static void setField(Object obj, Field field, Object value) {
field.setAccessible(true);
try {
field.set(obj, value);
} catch (IllegalAccessException e) {
log.error("set field failed", e);
e.printStackTrace();
}
}
}
根據實現思路寫程式碼
?注意 :以下程式碼未涉及解決迴圈依賴的問題!以下是 IoC 實現的核心程式碼,完整程式碼地址:https://github.com/Snailclimb/jsoncat 。
1.掃描指定包下的特定註解比如@Component
標記的類,並將這些類儲存起來。
掃描指定註解@RestController
和@Component
並儲存起來:
public class ClassFactory {
public static final Map<Class<? extends Annotation>, Set<Class<?>>> CLASSES = new ConcurrentHashMap<>();
//1.掃描指定包下的特定註解比如`@Component`標記的類,並將這些類儲存起來
public static void loadClass(String packageName) {
Set<Class<?>> restControllerSets = ReflectionUtil.scanAnnotatedClass(packageName, RestController.class);
Set<Class<?>> componentSets = ReflectionUtil.scanAnnotatedClass(packageName, Component.class);
CLASSES.put(RestController.class, restControllerSets);
CLASSES.put(Component.class, componentSets);
}
}
2.遍歷所有被特定註解比如@Component
標記的類,然後將這些類通過反射例項化並通過一個 Map 儲存起來,Map 的 key 為類名,value為類物件。
public final class BeanFactory {
public static final Map<String, Object> BEANS = new ConcurrentHashMap<>(128);
public static void loadBeans() {
// 2.遍歷所有被特定註解比如 @Component 標記的類,然後將這些類通過反射例項化並通過一個 Map 儲存起來,Map 的 key 為類名,value為類物件
ClassFactory.CLASSES.forEach((annotation, classes) -> {
if (annotation == Component.class) {
//將bean例項化, 並放入bean容器中
for (Class<?> aClass : classes) {
Component component = aClass.getAnnotation(Component.class);
String beanName = "".equals(component.name()) ? aClass.getName() : component.name();
Object obj = ReflectionUtil.newInstance(aClass);
BEANS.put(beanName, obj);
}
}
if (annotation == RestController.class) {
for (Class<?> aClass : classes) {
Object obj = ReflectionUtil.newInstance(aClass);
BEANS.put(aClass.getName(), obj);
}
}
});
}
}
3.再一次遍歷所有被特定註解比如@Component
標記的類,並獲取類中所有的欄位,如果類被 @Autowired
註解標記的話,就進行第 4 步。
public class DependencyInjection {
public static void dependencyInjection(String packageName) {
Map<String, Object> beans = BeanFactory.BEANS;
if (beans.size() == 0) return;
//3.再一次遍歷所有被特定註解比如 @Component 標記的類,並獲取類中所有的欄位,如果類被 `@Autowired` 註解標記的話,就進行第 4 步。
// 3.1.遍歷bean容器中的所有物件
beans.values().forEach(bean -> {
// 3.2.獲取物件所屬的類宣告的所有欄位/屬性
Field[] beanFields = bean.getClass().getDeclaredFields();
if (beanFields.length == 0) return;
//3.3.遍歷物件所屬的類宣告的所有欄位/屬性
for (Field beanField : beanFields) {
//3.4.判斷欄位是否被 @Autowired 註解標記
if (beanField.isAnnotationPresent(Autowired.class)) {
//4.通過欄位名 key,從bean容器中獲取對應的物件 value。
//4.1.欄位對應的型別
Class<?> beanFieldClass = beanField.getType();
//4.2.欄位對應的類名
String beanName = beanFieldClass.getName();
if (beanFieldClass.isAnnotationPresent(Component.class)) {
Component component = beanFieldClass.getAnnotation(Component.class);
beanName = "".equals(component.name()) ? beanFieldClass.getName() : component.name();
}
//4.3.從bean容器中獲取對應的物件
Object beanFieldInstance = beans.get(beanName);
//5.判斷獲取到的物件是否為介面。如果是介面的話,需要獲取介面對應的實現類,然後再將指定的實現類的例項化物件通過反射賦值給指定物件。如果不是介面的話,就直接將獲取到的物件通過反射賦值給指定物件。
if (beanFieldClass.isInterface()) {
//如果是介面,獲取介面對應的實現類
Set<Class<?>> subClasses = getSubClass(packageName, beanFieldClass);
//沒有實現類的話就丟擲異常
if (subClasses.size() == 0) {
throw new InterfaceNotHaveImplementedClassException("interface does not have implemented class exception");
}
//實現類只有一個話,直接獲取
if (subClasses.size() == 1) {
Class<?> aClass = subClasses.iterator().next();
beanFieldInstance = ReflectionUtil.newInstance(aClass);
}
//實現類多與一個的話,根據 Qualifier 註解的值獲取
if (subClasses.size() > 1) {
Class<?> aClass = subClasses.iterator().next();
Qualifier qualifier = beanField.getDeclaredAnnotation(Qualifier.class);
beanName = qualifier == null ? aClass.getName() : qualifier.value();
beanFieldInstance = beans.get(beanName);
}
}
// 如果最後獲取到的欄位物件為null,就丟擲異常
if (beanFieldInstance == null) {
throw new CanNotDetermineTargetBeanException("can not determine target bean");
}
//通過反射設定指定物件中的指定欄位的值
ReflectionUtil.setField(bean, beanField, beanFieldInstance);
}
}
});
}
/**
* 獲取介面對應的實現類
*/
@SuppressWarnings("unchecked")
public static Set<Class<?>> getSubClass(String packageName, Class<?> interfaceClass) {
Reflections reflections = new Reflections(packageName);
return reflections.getSubTypesOf((Class<Object>) interfaceClass);
}
}
我整理了一份優質原創PDF資源免費分享給大家,大部分內容都是我的原創,少部分來自朋友。
下載地址:https://cowtransfer.com/s/fbed14f0c22a4d 。
我是 Guide 哥,一 Java 後端開發,會一點前端,自由的少年。我們下期再見!微信搜“JavaGuide”回覆“面試突擊”領取我整理的 4 本原創PDF