自定義IoC容器
阿新 • • 發佈:2020-07-31
自定義IoC容器
- 建立註解
- 提取標記物件
- 實現容器
- 依賴注入
下面分步實現
一、建立四個常用的註解@Controller、@Service、@Component、@Repository
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Controller { } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Service { } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Component { } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Repository { }
二、提取被註解標記的類
(1). 指定範圍,獲取範圍內的所有類
-
獲取類載入器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
-
通過類載入器獲取到載入的資源資訊
//通過包名獲取資源的地址 URL url = classLoader.getResource(packageName.replace(".", "/")); //這裡檢視URL的getResource原始碼可以發現,URL是通過'/'來進行分割的,所以要將包名中的'.'替換成'/' /** * Finds the resource with the given name. A resource is some data * (images, audio, text, etc) that can be accessed by class code in a way * that is independent of the location of the code. * * <p> The name of a resource is a '<tt>/</tt>'-separated path name that * identifies the resource. * * <p> This method will first search the parent class loader for the * resource; if the parent is <tt>null</tt> the path of the class loader * built-in to the virtual machine is searched. That failing, this method * will invoke {@link #findResource(String)} to find the resource. </p> * * @apiNote When overriding this method it is recommended that an * implementation ensures that any delegation is consistent with the {@link * #getResources(java.lang.String) getResources(String)} method. * * @param name * The resource name * * @return A <tt>URL</tt> object for reading the resource, or * <tt>null</tt> if the resource could not be found or the invoker * doesn't have adequate privileges to get the resource. * * @since 1.1 */ public URL getResource(String name) { URL url; if (parent != null) { url = parent.getResource(name); } else { url = getBootstrapResource(name); } if (url == null) { url = findResource(name); } return url; }
-
依據不同的資源型別,採用不同的方式獲取資源的集合
//首先過濾出檔案型別的資源 //然後通過extractClassFile函式遞迴提取所需的所有檔案 //這裡只file型別的資源 if(url.getProtocol().equalsIgnoreCase(FILE_PROTOCOL)){ classSet = new HashSet<>(); File packageDirectory = new File(url.getPath()); if(!packageDirectory.isDirectory()){ return null; } extractClassFile(classSet, packageDirectory, packageName); } /** * 遞迴獲取目標package下面的所有class檔案 * @param emptyClassSet 裝載目標類的集合 * @param fileSource 檔案或者目錄 * @param packageName 包名 */ private static void extractClassFile(Set<Class<?>> emptyClassSet, File fileSource, String packageName) { //如果不是資料夾,直接返回 if(!fileSource.isDirectory()){ return; } //如果是資料夾,則列出所有檔案和子資料夾,獲取到的是資料夾型別的File陣列 File[] files = fileSource.listFiles(new FileFilter() { @Override public boolean accept(File file) { //如果是資料夾,則被過濾提取出來 if(file.isDirectory()){ return true; }else { //獲取檔案的絕對值路徑 String absoluteFilePath = file.getAbsolutePath(); if(absoluteFilePath.endsWith(".class")){ //class檔案直接載入 addToClassSet(absoluteFilePath); } return false; } } private void addToClassSet(String absoluteFilePath) { //1.從class檔案的絕對值路徑中提取包含了package的類名 //將現在的'/'或'\'替換為包名中的'.' absoluteFilePath = absoluteFilePath.replace(File.separator, "."); String className = absoluteFilePath.substring(absoluteFilePath.indexOf(packageName)); className = className.substring(0, className.lastIndexOf(".")); //2.通過反射獲取Class物件,並加入classSet裡 Class<?> targetClass = loadClass(className); emptyClassSet.add(targetClass); } }); if(files != null){ //遞迴呼叫 for (File file : files) { extractClassFile(emptyClassSet, file, packageName); } } } //loadClass通過反射獲取Class物件 /** * 獲取class物件 * @param className 類名 * @return class物件 */ public static Class<?> loadClass(String className){ try { return Class.forName(className); } catch (ClassNotFoundException e) { log.error("load class error ", e); throw new RuntimeException(e); } }
三、實現統一容器管理
- 首先建立容器
容器的組成部分:
- 儲存Class物件及其例項
- 容器的載入
- 容器的對Class及例項的操作
第一步:需要用同一個容器例項將所有目標管理起來,所以容器需要寫成單例模式,這裡採用裝備內部列舉類的餓漢模式來抵禦序列化和反射的攻擊:
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unused")
public class BeanContainer {
/**
* 獲取bean容器例項
* @return 單例BeanContainer
*/
public static BeanContainer getInstance(){
return ContainHolder.HOLDER.instance;
}
private enum ContainHolder{
HOLDER;
private final BeanContainer instance;
ContainHolder(){
instance = new BeanContainer();
}
}
}
//使用@NoArgsConstructor(access = AccessLevel.PRIVATE)新增私有的建構函式
首先在容器中定義兩個變數,其中beanMap用來存放被標記的目標物件的Map,BEAN_ANNOTATION是被載入的註解的列表,例子中只寫了四個
private final Map<Class<?>, Object> beanMap = new ConcurrentHashMap<>();
private static final List<Class<? extends Annotation>> BEAN_ANNOTATION =
Arrays.asList(Component.class, Controller.class, Service.class, Repository.class);
第二步:開始掃描載入bean,其中synchronized防止多執行緒訪問衝突,:
/**
* 掃描載入所有Bean
* @param packageName 包名
*/
public synchronized void loadBeans(String packageName){
//判斷bean容器是否被載入過
if(loaded){
log.warn("BeanContainer has been loaded.");
return;
}
//通過上面講解的extractPackageClass方法獲取所有類物件
Set<Class<?>> classSet = ClassUtil.extractPackageClass(packageName);
//進行判空
if(ValidationUtil.isEmpty(classSet)){
log.warn("extract nothing from packageName" + packageName);
return;
}
for (Class<?> clazz : classSet) {
for (Class<? extends Annotation> annotation : BEAN_ANNOTATION) {
//如果類上面標記了定義的註解
if(clazz.isAnnotationPresent(annotation)){
//將目標類本身作為鍵,例項作為值,放入到beanMap中
beanMap.put(clazz, ClassUtil.newInstance(clazz, true));
break;
}
}
}
loaded = true;
}
//beanMap.put(clazz, ClassUtil.newInstance(clazz, true));
//這裡有一個newInstance方法利用反射建立例項
/**
* 例項化Class
* @param clazz Class
* @param accessible 是否支援創建出私有clazz物件的例項
* @param <T> clazz的型別
* @return 類的例項
*/
public static <T> T newInstance(Class<T> clazz, boolean accessible){
try {
Constructor<T> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(accessible);
return constructor.newInstance();
} catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
log.error("newInstance error", e);
throw new RuntimeException(e);
}
}
第三步:實現容器的操作方式
-
增加、刪除操作
/** * 新增一個class物件及其bean例項 * @param clazz Class物件 * @param bean bean例項 * @return 原有的bean例項,沒有則返回null */ public Object addBean(Class<?> clazz, Object bean){ return beanMap.put(clazz, bean); } /** * 移除一個IOC容器管理的物件 * @param clazz Class物件 * @return 刪除的bean例項,沒有則返回null */ public Object removeBean(Class<?> clazz){ return beanMap.remove(clazz); }
-
根據Class獲取對應例項
/** * 根據Class物件獲取Bean例項 * @param clazz Class物件 * @return 相應Bean例項 */ public Object getBean(Class<?> clazz){ return beanMap.get(clazz); }
-
獲取所有的Class及其例項
/** * 獲取容器管理的所有物件的集合 * @return Class集合 */ public Set<Class<?>> getClasses(){ return beanMap.keySet(); } /** * 獲取所有Bean集合 * @return Bean集合 */ public Set<Object> getBeans(){ return new HashSet<>(beanMap.values()); }
-
通過註解獲取被標註的Class
/** * 根據註解篩選出Bean的Class集合 * @param annotation 註解 * @return Class集合 */ public Set<Class<?>> getClassesByAnnotation(Class<? extends Annotation> annotation){ //1.獲取beanMap的所有物件 Set<Class<?>> keySet = getClasses(); if(ValidationUtil.isEmpty(keySet)){ log.warn("nothing in beanMap"); return null; } //2.通過註解篩選出被註解標記的物件,並新增到classSet裡 Set<Class<?>> classSet = new HashSet<>(); for (Class<?> clazz : keySet) { //類是否被相關注解標記 if(clazz.isAnnotationPresent(annotation)){ classSet.add(clazz); } } return classSet.size() > 0?classSet:null; }
-
通過超類獲取對應子類的Class
/** * 通過介面或者父類獲取實現類或子類的Class集合,不包括其本身 * @param interfaceOrClass 介面或父類 * @return Class集合 */ public Set<Class<?>> getClassesBySuper(Class<?> interfaceOrClass){ //1.獲取beanMap的所有物件 Set<Class<?>> keySet = getClasses(); if(ValidationUtil.isEmpty(keySet)){ log.warn("nothing in beanMap"); return null; } //2.判斷keySet裡面的元素是否是傳入的介面或者類的子類,並新增到classSet裡 Set<Class<?>> classSet = new HashSet<>(); for (Class<?> clazz : keySet) { if(interfaceOrClass.isAssignableFrom(clazz) && !clazz.equals(interfaceOrClass)){ classSet.add(clazz); } } return classSet.size() > 0?classSet:null; }
-
通過容器載體儲存Class數量
/** * Bean例項數量 * @return 數量 */ public int size(){ return beanMap.size(); }
這樣一來,我們的容器已經實現完畢,下面進行測試:
//定義一個測試介面
public interface DemoService {
String getName();
}
//介面的實現類
@Service
public class DemoServiceImpl implements DemoService {
@Override
public String getName() {
return "DemoServiceImpl";
}
}
//以及呼叫介面的Controller
@Controller
public class ControllerDemo {
private DemoService demoService;
String getServiceName(){
return demoService.getName();
}
}
//編寫測試類進行除錯
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BeanContainerTest {
private static BeanContainer beanContainer;
@BeforeAll
static void init(){
beanContainer = BeanContainer.getInstance();
}
@DisplayName("根據類獲取其例項:getBeanTest")
@Order(1)
@Test
public void getBeanTest(){
beanContainer.loadBeans("com.joker");
ControllerDemo controller = (ControllerDemo) beanContainer.getBean(ControllerDemo.class);
Assertions.assertNotNull(controller);
}
}
如下圖發現ControllerDemo已經成功從容器中獲取到了,但是其內部的DemoService卻是null,那是因為我們只實現了容器的管理,還沒實現依賴的注入
四、實現依賴注入
- 定義相關的註解標籤
這裡以@Autowired註解示例
/**
* Autowired目前僅支援成員變數的注入
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
String value() default "";
}
-
實現建立被註解標記的成員變數例項,並將其注入到成員變數裡
public void doIoc(){ if(ValidationUtil.isEmpty(beanContainer.getClasses())){ log.warn("empty classSet in BeanContainer."); return; } //1.遍歷Bean容器中所有的Class物件 for (Class<?> clazz : beanContainer.getClasses()) { //2.遍歷Class物件的所有成員變數 Field[] fields = clazz.getDeclaredFields(); if(ValidationUtil.isEmpty(fields)){ continue; } for (Field field : fields) { //3.找出被Autowired標記的成員變數 if(field.isAnnotationPresent(Autowired.class)){ Autowired autowired = field.getAnnotation(Autowired.class); String autowiredValue = autowired.value(); //4.獲取這些成員變數的型別 Class<?> fieldClass = field.getType(); //5.獲取這些成員變數的型別在容器裡對應的例項 Object fieldValue = getFieldInstance(fieldClass, autowiredValue); if(fieldValue == null){ throw new RuntimeException("unable to inject relevant type, target field is:" + fieldClass.getName()); } //6.通過反射將對應的成員變數例項注入到成員變數所在類的例項裡 Object targetObject = beanContainer.getBean(clazz); ClassUtil.setField(field, targetObject, fieldValue, true); } } } } //5.獲取這些成員變數的型別在容器裡對應的例項 /** * 根據Class物件在beanContainer裡獲取其例項或者實現類 * @param fieldClass Class物件 * @param autowiredValue 註解中的值 * @return 例項或實現類 */ private Object getFieldInstance(Class<?> fieldClass, String autowiredValue) { Object fieldValue = beanContainer.getBean(fieldClass); if(fieldValue != null){ return fieldValue; } Class<?> implementedClass = getImplementClass(fieldClass, autowiredValue); if (implementedClass != null){ return beanContainer.getBean(implementedClass); } return null; } /** * 獲取介面的實現類 * @param fieldClass 介面 * @param autowiredValue 註解中的值 * @return 實現類 */ private Class<?> getImplementClass(Class<?> fieldClass, String autowiredValue) { Set<Class<?>> classSet = beanContainer.getClassesBySuper(fieldClass); if(ValidationUtil.isEmpty(classSet)){ return null; } if(ValidationUtil.isEmpty(autowiredValue)){ if(classSet.size() == 1){ return classSet.iterator().next(); } //如果多於兩個實現類且未被指定,丟擲異常 throw new RuntimeException("multiple implement classes for " + fieldClass.getName() + "please set @Autowired's value to pick one"); } for (Class<?> clazz : classSet) { if(autowiredValue.equals(clazz.getSimpleName())){ return clazz; } } //沒有被容器管理 return null; } //6.通過反射將對應的成員變數例項注入到成員變數所在類的例項裡 ClassUtil.setField(field, targetObject, fieldValue, true); //寫在了ClassUtil類中 /** * 設定類的屬性值 * @param field 成員變數 * @param target 類例項 * @param value 成員變數的值 * @param accessible 是否允許設定私有屬性 */ public static void setField(Field field, Object target, Object value, boolean accessible){ field.setAccessible(accessible); try { field.set(target, value); } catch (IllegalAccessException e) { log.error("set field error", e); throw new RuntimeException(e); } }
-
依賴注入的使用
上述依賴注入程式編寫完畢,只需在ControllerDemo的Service變數上面加一個註解,即使用@Autowired:
@Controller
public class ControllerDemo {
@Autowired
private DemoService demoService;
public String getServiceName(){
return demoService.getName();
}
}
然後編寫測試類:
public class DependencyInjectorTest {
@DisplayName("依賴注入:doIoc")
@Test
public void doIocTest(){
BeanContainer beanContainer = BeanContainer.getInstance();
beanContainer.loadBeans("com.joker");
Assertions.assertTrue(beanContainer.isLoaded());
ControllerDemo controllerDemo = (ControllerDemo) beanContainer.getBean(ControllerDemo.class);
Assertions.assertNotNull(controllerDemo);
new DependencyInjector().doIoc();
Assertions.assertNotNull(controllerDemo.getServiceName());
}
}
除錯執行結果如圖:
結果無誤,此篇結。