1. 程式人生 > 實用技巧 >自定義IoC容器

自定義IoC容器

自定義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). 指定範圍,獲取範圍內的所有類

  1. 獲取類載入器

     ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    
  2. 通過類載入器獲取到載入的資源資訊

     //通過包名獲取資源的地址
     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;
     }
    
  3. 依據不同的資源型別,採用不同的方式獲取資源的集合

     //首先過濾出檔案型別的資源
     //然後通過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);
         }
     }
    

三、實現統一容器管理

  1. 首先建立容器

容器的組成部分:

  • 儲存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());
    }
}

除錯執行結果如圖:

結果無誤,此篇結。