深入了解IoC

前言

IoC已經是目前業(yè)界非常主流的一種容器技術,全稱為Inversion of Control,中文翻譯為“控制反轉”悔雹。它還有一種另外的術語叫Dependency Injection(依賴注入)。這些概念用好萊塢的一個原則來描述就是“Don‘t call us腌零,we will call you.”梯找,這句話其實很好的表達了IoC的含義。但往往讓很多初學者難以理解益涧,過于抽象锈锤。所以本文的內容將其含義一步步進行剖析和解釋,為什么需要IoC闲询,以及如何實現(xiàn)一個簡單的IoC容器久免。

1. 理解概念

在深入學習之前,我們需要先鋪墊一些必要的概念扭弧,這有助于大家對整個知識面的貫穿和理解阎姥。如果你已經掌握了依賴倒置原則和控制反轉,可以從第2小節(jié)開始閱讀鸽捻。

1.1 依賴倒置原則

依賴倒置原則(DIP)是面向對象的五大設計原則(SOLID)之一呼巴,這個原則奠定了IoC的核心思想和理念。DIP的核心告訴我們御蒲,“高層模塊不依賴于低層模塊伊磺,兩者應該依賴于抽象”。我們如何理解這句話呢删咱?首先我們看看下面的代碼片段屑埋,并分析一下代碼中所存在的一些問題。

public class Wrench {
  public void repair(){
    System.out.println("Repairing...");
  } 
}
public class Worker {
  public void work(Wrench wrench){
    wrench.repair();
  }
}

上面的代碼很簡單痰滋,工人(Worker)依賴于一個扳手(Wrench)來完成修理的工作摘能。Worker是調用方,稱之為高層模塊敲街。Wrench是被調用方团搞,稱之為低層模塊。

image

這個例子從功能實現(xiàn)的層面上看似乎沒什么大問題多艇,但仔細思考一下逻恐,在實際的業(yè)務場景中,Worker在完成一個具體的修理工作時可能需要使用不同的工具,Wrench只是其中之一复隆,如果此時更換另一種工具比如鉗子(Pliers),那么就必須修改Worker類拨匆,這樣就違反了DIP,同時還違反了OCP(對內修改是關閉的挽拂,對外擴展是開放的)惭每。如果要遵循DIP,Worker(高層模塊)就不應該直接依賴于Wrench(低層模塊)亏栈,讓他們兩者都依賴一個抽象台腥,我們再來看看修改后的代碼。

public abstract class Tools {
  public abstract void repair();
}
public class Wrench extends Tools{
  public void repair(){
    System.out.println("Use wrench repairing...");
  } 
}
public class Pliers extends Tools{
  public void repair(){
    System.out.println("Use pliers repairing...");
  } 
}
public class Worker {  
  public void work(Tools tools){
    tools.repair();
  }
}

上面的例子中抽象出了Tools類绒北,并包含一個repair的抽象方法黎侈。而Worker類的work方法不再直接依賴Wrench,而是依賴于Tools這個抽象類闷游,它不需要關心Tools的子類有哪些峻汉,運行時決定由具體哪個子類對象來執(zhí)行。Wrench只需要繼承抽象Tools類并實現(xiàn)標準的repair方法完成自己相關的業(yè)務邏輯储藐。這樣高層模塊不再依賴具體的低層細節(jié)俱济,兩者都面向的是一個抽象。當需要更換工具(擴展功能)時钙勃,只需要編寫新的類(例如Pliers)繼承Tools即可蛛碌,而Worker類是不需要的修改的。這樣的編碼就很好的遵循了DIP以及OCP辖源。

image

1.2 控制反轉

前面的例子解釋了DIP的思想和原則蔚携,如果要運行以上的程序,我們還需要在客戶端代碼中維護Worker以及Tools的創(chuàng)建以及相關的依賴關系克饶。

public class Main {
  public static void main(String[] args){
    Tools tools = new Wrench();
    Worker worker = new Worker();
    worker.work(tools);
  }
}

仔細觀察以上的代碼酝蜒,我們發(fā)現(xiàn)其中包含了對象維護的兩個工作:

  • 創(chuàng)建Wrench以及Worker的實例
  • 完成Worker與Wrench之間的依賴關系

因此這里又出現(xiàn)一個潛在的問題,就是當需要更換Wrench的時候矾湃,又需要更改Main這個類的代碼(搞半天亡脑,又回到了問題的根源)。這其中主要的問題就是上面的兩個工作(對象的創(chuàng)建以及對象之間的依賴關系)都是以硬編碼的方式出現(xiàn)在程序中邀跃。因此可以將這兩個工作移交給一個獨立的組件去完成霉咨,它核心職責就是完成對象的創(chuàng)建以及對象之間依賴關系的維護和管理,那么這個組件我們將其稱之為“容器”拍屑。我們先看看下面的代碼片段途戒。

public abstract class Tools {
  public abstract void repair();
}
public class Wrench extends Tools{
  public void repair(){
    System.out.println("use wrench repairing...");
  } 
}
public class Worker {  
  //容器會通過這個方法自動將Tools的子類對象傳遞進來
  private void setTools(Tools tools){
    this.tools = tools;
  }
  
  public void work(){
    tools.repair();
  }
}
public class Main {
  public static void main(String[] args){
    //工廠容器
    BeanFactory factory = new BeanFactory();
    //從容器中直接獲取Worker對象
    Worker worker = factory.getBean("worker", Worker.class);
    worker.work();
  }
}

使用容器的好處在于僵驰,當我們需要Worker對象的時候喷斋,不需要自己來創(chuàng)建唁毒,通過容器提供的getBean方法直接獲取即可,因為容器已經幫我們創(chuàng)建好了Worker星爪。如果Worker類需要依賴Tools來完成具體事情浆西,則可以在Worker中提供一個set方法(不一定是set方法,也可以是構造方法或其他的方式)移必,讓容器通過這個方法將Tools的具體子類或實現(xiàn)類傳遞進來室谚,這樣就給Worker裝配的Tools毡鉴。而容器在調用set方法并傳入一個Tools實例到Worker中的這個過程就是所謂的控制反轉崔泵,也叫依賴注入(你可以理解為容器通過某種手段將Tools的實例注入到了Worker中),這個容器我們也將其稱之為IoC容器猪瞬,它很好的完成了對象的創(chuàng)建和對象之間依賴的工作憎瘸。

2. 實現(xiàn)簡單的IoC容器

通過前面的學習我們都很清楚IoC容器的核心職責是負責對象的管理以及對象之間的依賴。也可以將其分開理解為容器就是負責對象的創(chuàng)建和管理陈瘦,IoC則是完成對象之間的依賴注入幌甘。而依賴注入的前提是要有容器的支撐,因為任何需要注入的對象都必須從容器中獲取痊项,則實現(xiàn)的第一步是先編寫一個管理對象容器锅风。

2.1 實現(xiàn)BeanFactory

既然所有對象的創(chuàng)建過程都交給了容器,那么它不就是典型的工廠嗎鞍泉?沒錯皱埠,實現(xiàn)這個容器本身也是對工廠模式的一種體現(xiàn)。

2.1.1 哪些Bean交給容器管理

在一個項目當中不是所有的類都需要納入容器管理咖驮,我們可以配置哪些類需要讓容器管理边器,這個配置的過程可以使用Java的注解或者xml來完成,所有納入容器管理的對象我們統(tǒng)稱為Bean托修。比如使用xml配置:

<bean id="people" class="edu.demo.People"/>
<bean id="wrench" class="edu.demo.Wrench"/>

id表示這個bean在容器中的唯一標識忘巧,class則是這個類的全限定類名,接著就可以使用java對這個xml進行解析睦刃。當然砚嘴,你也可以使用Annotation來配置,那么首先可以先自定義一個注解涩拙,然后將這個注解標注在類上面际长,后續(xù)通過反射對注解進行解析(后面的實現(xiàn)過程都以注解配置的方式來實現(xiàn))。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
    public String value();
}

自定義一個@Component注解吃环,@Retention(RetentionPolicy.RUNTIME)表示這個注解會在運行時一直保留也颤,@Target(ElementType.TYPE)表示這個注解只可以標注在類上。并且這個注解有一個value屬性類型為String郁轻。value屬性的作用是用于定義當前類在容器中的唯一標識(類似xml配置中的id)翅娶。

@Component(value="people")
public class People {
  ...
}

定義好注解后就可以應用在類上了文留。像上面的例子,如果類上面定義了@Component注解竭沫,就表示這個類是受容器管理的燥翅,并且這個類在容器中有一個唯一的標識“people”。如果注解中只有一個屬性蜕提,并且屬性名為value的情況下森书,那么在定義時可以省略,例如@Component("people")谎势。

2.1.2 Bean的創(chuàng)建形式

當客戶端從容器中獲取Bean實例的時候凛膏,容器并不一定每一次都新建一個,它可以事先創(chuàng)建一個并一直駐留在容器中脏榆,每次獲取的時候返回同一個實例猖毫,可以達到實例復用的目的,也可以節(jié)省內存的開銷须喂,這種做法類似與單例模式吁断。但是這種方式也存在一個問題,就是線程安全坞生。由于在運行時的數(shù)據(jù)區(qū)中只有一個實例仔役,在多線程的情況下,這個對象是被多線程所共享的是己,所以在使用時要求這個實例是線程安全的又兵。當然,在很多場景我們也會要求容器每次都返回一個新的實例赃泡。這種方式創(chuàng)建出來的實例并不會駐留在容器中寒波,用完即扔的效果。所以這就必須要求容器對Bean提供不同的管理方式升熊。

2.1.3 Bean的作用域

既然容器可以以不同的方式來構建和管理Bean實例俄烁,那么這就涉及到另一個概念“作用域”。你可以理解為作用域就是Bean實例的一個生命周期或者存活時間级野。試想一下页屠,以單例的方式創(chuàng)建的對象會一直駐留在容器中,那么就表示直到這個容器關閉或者銷毀的時候Bean實例才會跟隨著銷毀蓖柔。Bean的有效存活時間是在整個容器的創(chuàng)建到關閉的有效范圍辰企。如果每次獲取的是一個新的實例,由于不會駐留在容器中况鸣,那么Bean的有效存活時間為就是這個對象使用完或者無引用的時候等待jvm的回收牢贸。我們可以將單例的Bean的作用域命名為singleton,將每次新建的Bean的作用域命名為prototype镐捧,其他的作用域還可以包括web中的request或者session等作用域潜索,這里為了簡化只對singleton和prototype進行實現(xiàn)臭增。那么問題來了,如何告訴容器Bean的作用域是哪一種呢竹习?沒錯誊抛,還是使用注解配置,我們看看下面的代碼片段整陌。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Scope {
    String value() default "singleton";
}
@Component(value="people")
@Scope
public class People {
  ...
}

我們可以自定義一個@Scope注解,value屬性用于指定作用域的值拗窃,默認為singleton,這個注解同樣只能標注在類上泌辫。這樣容器在解析注解的時候就知道以什么樣的方式來創(chuàng)建Bean實例以及管理它的作用域随夸。如果類上沒有標注@Scope注解,可以讓容器默認以單例的方式來創(chuàng)建甥郑。

2.1.4 類掃描

前面我們使用自定義注解來標識容器是如何創(chuàng)建和管理Bean實例逃魄,接下來就是進行類掃描荤西。掃描的目的就是為了收集當前項目下以及所依賴的jar文件中所有class的全限定類名澜搅。(為什么要掃描依賴的jar文件?因為我們有可能將標識@Component注解的類最終生成jar文件讓其他的項目依賴使用)我們看看下面的ScanUtil類邪锌。

public class ScanUtil {

    private static final Set<String> classNames = new HashSet<String>();

    /**
     * 依據(jù)指定的包名掃描包中以及子包中所有的類
     * @param packageName 包名
     * @return 全限定類名的集合
     */
    public static Set<String> scan(String packageName) {
        if(packageName == null){
            throw new RuntimeException("The path can not be null.");
        }
        String packagePath = packageName.replace(".", "/");
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        try {
            Enumeration<URL> urls = loader.getResources(packagePath);
            while(urls.hasMoreElements()){
                URL url= urls.nextElement();
                if("file".equals(url.getProtocol())){
                    scanFromDir(url.getPath(), packageName);
                }
                if("jar".equals(url.getProtocol())){
                    JarURLConnection connection = (JarURLConnection)url.openConnection();
                    scanFromJar(connection.getJarFile());
                }
            }
        } catch (IOException e) {
            throw new RuntimeException("Resolve path error.", e);
        }

        return classNames;
    }

    /**
     * 掃描目錄
     * @param filePath 文件目錄
     * @param packageName 包名
     */
    private static void scanFromDir(String filePath, String packageName) throws UnsupportedEncodingException{
        filePath = URLDecoder.decode(filePath, "utf-8");
        packageName = URLDecoder.decode(packageName, "utf-8");
        File[] files = new File(filePath).listFiles();
        packageName = packageName + ".";
        for (File childFile : files) {
            if (childFile.isDirectory()) {
                scanFromDir(childFile.getPath(), packageName + childFile.getName());
            } else {
                String fileName = childFile.getName();
                if (fileName.endsWith(".class")) {
                    if(packageName.charAt(0) == '.'){
                        packageName = packageName.substring(1, packageName.length());
                    }
                    String className = packageName + fileName.replace(".class", "");
                    classNames.add(className);
                }
            }
        }
    }

    /**
     * 掃描jar文件
     * @param jarFile
     */
    private static void scanFromJar(JarFile jarFile) {
        Enumeration<JarEntry> files = jarFile.entries();
        while (files.hasMoreElements()) {
            JarEntry entry = files.nextElement();
            if (entry.getName().endsWith(".class")){
                String className = entry.getName().replace("/", ".").replace(".class", "");
                classNames.add(className);
            }
        }
    }
}

scan為核心的掃描方法勉躺,依據(jù)傳入的包名將其解析為URL枚舉進行遍歷,然后根據(jù)url對象的Protocol來決定是對目錄還是jar進行掃描觅丰。如果Protocol是file則調用scanFromDir方法饵溅,如果是jar則調用scanFromJar方法。

2.1.5 BeanDefinition

掃描類的目的是為了要過濾出哪些類上面標注了@Component注解妇萄,因為只有標注了這個注解的類才能納入容器的管理蜕企。并且還需要將這些類的Class對象以及還有可能標注的@Scope作用域(后面會詳細講解作用的概念)等信息收集保存起來,容器在初始化時需要依據(jù)這些信息來構建Bean實例冠句。問題是如何保存這些信息呢轻掩?這就是BeanDefinition的作用,每當解析到一個帶有@Component注解的類懦底,那么就將它的Class對象以及@Scope信息封裝到一個BeanDefinition中保存唇牧,容器就會根據(jù)這個BeanDefinition來創(chuàng)建Bean實例。

public class BeanDefinition {

    /**
     * bean的作用域(創(chuàng)建方式)
     */
    private String scope;

    /**
     * bean的Class
     */
    private Class<?> beanClass;

    public String getScope() {
        return scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }

    public Class<?> getBeanClass() {
        return beanClass;
    }

    public void setBeanClass(Class<?> beanClass) {
        this.beanClass = beanClass;
    }
}

通過上面的代碼不難看出聚唐,BeanDefinition就是一個很普通的Javabean丐重,它僅僅封裝了需要管理對象的Class以及作用域信息。每創(chuàng)建一個BeanDefinition都代表一個Bean的描述定義杆查,后續(xù)通過描述定義來構建具體的實例扮惦。

2.1.6 編寫容器

前期的鋪墊工作已經差不多了,下面開始將編寫最核心的組件"容器"亲桦。

public class BeanFactory {
    /**
     * 存放bean的描述
     */
    final Map<String, BeanDefinition> definitionMap = new ConcurrentHashMap<>();

    /**
     * 存放單例bean的實例
     */
    final Map<String, Object> singletonMap = new ConcurrentHashMap<>();

    /**
     * 在構造方法中初始化并構建所有bean描述
     * 以及單例的bean
     *
     * @param path 掃描路徑
     */
    public BeanFactory(String path) {
        Set<String> classNames = ScanUtil.scan(path);
        //初始化原型
        initDefinitionMap(classNames);
        //初始化單例
        initSingleton();
    }

    /**
     * 根據(jù)掃描的類名進行解析崖蜜,找出帶有@Component注解的類掺栅,并構建成
     * BeanDefinition實例,保存到definitionMap集合中
     */
    private void initDefinitionMap(Set<String> classNames) {
        for (String className : classNames) {
            Class<?> beanClass = getClass(className);
            //檢查beanClass是否標注了@Component注解
            if (beanClass.isAnnotationPresent(Component.class)) {
                //獲取@Component注解的value屬性的值纳猪,這個值作為bean在容器的唯一標識
                String beanName = beanClass.getAnnotation(Component.class).value();
                //如果容器已經存在bean氧卧,則拋出異常
                if (definitionMap.containsKey(beanName)) {
                    throw new RuntimeException(
                            "conflicts with existing, non-compatible bean definition of same name and class ["
                                    + beanClass + "]");
                } else {
                    definitionMap.put(beanName,
                            createBeanDefinition(beanClass));
                }
            }
        }
    }

    /**
     * 根據(jù)權限頂類名獲取Class對象
     *
     * @param className
     * @return
     */
    private Class<?> getClass(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Can not find the class name " + className + " to build the description.");
        }
    }

    /**
     * 構建bean描述定義,將bean的scope以及類名封裝到BeanDefinition中
     * 創(chuàng)建的Bean描述會放入definitionMap的集合中保存
     * Bean的類名作為集合的key,而整個BeanDefinition對象作為value
     *
     * @param beanClass
     */
    private BeanDefinition createBeanDefinition(Class<?> beanClass) {
        // 創(chuàng)建BeanDefinition
        BeanDefinition definition = new BeanDefinition();
        //設置Bean的Class對象
        definition.setBeanClass(beanClass);
        //設置Bean的作用域
        definition.setScope(resolveScope(beanClass));
        return definition;
    }

    /**
     * 解析Scope,如果bean的class上指定了Scope注解,則將@Scope的value屬性值作為Bean的創(chuàng)建方式
     * 否則Bean的默認創(chuàng)建方式將使用單例
     */
    private String resolveScope(Class<?> beanClass) {
        String scope = (beanClass.isAnnotationPresent(Scope.class)) ? beanClass
                .getAnnotation(Scope.class).value() : "singleton";
        return scope;
    }

    /**
     * 初始化SINGLETON實例放入bean容器中
     */
    private void initSingleton() {
        for (String beanName : definitionMap.keySet()) {
            BeanDefinition definition = definitionMap.get(beanName);
            if ("singleton".equals(definition.getScope())) {
                Object bean = newInstance(definition);
                singletonMap.put(beanName, bean);
            }
        }
    }

    /**
     * 根據(jù)描述定義創(chuàng)建Bean實例
     * @param definition
     * @return
     */
    private Object newInstance(BeanDefinition definition) {
        try {
            return definition.getBeanClass().newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Create bean instance fail.", e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Create bean instance fail.", e);
        }
    }

    /**
     * 獲取bean實例
     *
     * @param beanName
     * @return
     */
    public Object getBean(String beanName) {
        return doGetBean(beanName);
    }

    /**
     * 獲取bean實例(泛型)
     *
     * @param beanName
     * @param clazz
     * @return
     */
    public <T> T getBean(String beanName, Class<T> clazz) {
        return (T) doGetBean(beanName);
    }

    /**
     * 從容器中獲取Bean的BeanDefinition
     * 如果Bean的BeanDefinition的scope為singleton,則從singletonMap中獲取單例
     * 否則以原型的方式創(chuàng)建并返回
     */
    private Object doGetBean(String beanName) {
        BeanDefinition definition = definitionMap.get(beanName);
        if("singleton".equals(definition.getScope())){
            return singletonMap.get(beanName);
        }
        return newInstance(definition);
    }

說一下整體思路氏堤,BeanFactory中維護了definitionMap和singletonMap兩個map集合沙绝,這兩個集合都作為容器的一部分存在,只是存放的內容不一樣鼠锈。definitionMap用來存放所有Bean的描述定義(BeanDefinition)闪檬,singletonMap則存放所有Scope為singleton的Bean實例。在創(chuàng)建BeanFactory的同時购笆,通過構造方進行容器的初始化粗悯。依據(jù)傳入的包名進行類掃描,接著調用initDefinitionMap方法執(zhí)析并初始化所有的BeanDefinition保存到definitionMap中同欠。然后調用initSingleton方法初始化所有單例的Bean(注意样傍,在初始化所有單例的過程中也是依據(jù)先前構建好的BeanDefinition來創(chuàng)建)。完成這些步驟铺遂,容器就初始化完成了衫哥。但容器還要需要對外提供一個可以讓客戶端從容器中獲取Bean實例的方法。注意觀察代碼中的兩個getBean方法(兩個方法區(qū)別只是對泛型的支持)襟锐,它們都調用了doGetBean的私有方法撤逢,這個方法會判斷是以單例還是原型的方式來構建Bean實例,如果scope為singleton粮坞。那么直接從singletonMap中獲取先前初始化好的對象并返回蚊荣,否則調用createBean方法依據(jù)BeanDefinition創(chuàng)建一個原型的實例并返回。到此莫杈,簡單的容器就實現(xiàn)好了互例。

2.1.7 測試容器

容器編寫好后我們需要測試一下容器的運行效果。首先將編寫好的所有源碼編譯并導出為一個jar文件姓迅,在測試項目中依賴進來敲霍。這里使用先前的案例來進行簡單的單元測試。

@Component("worker")
public class Worker {  
  public void work(Tools tools){
    tools.repair();
  }
}

在Worker類上標注@Component注解丁存,并給value屬性賦值一個worker,表示在容器中的唯一標識肩杈。這里沒有使用@Scope注解,因此容器默認就是以單例的方式來構建Worker實例解寝。

public class BeanFactoryTest {
  @Test
  public void testGetBean(){
    //創(chuàng)建工廠容器
    BeanFactory factory = new BeanFactory("edu.demo");
    //從容器中直接獲取Worker對象
    Worker w1 = factory.getBean("worker", Worker.class);
    Worker w2 = factory.getBean("worker", Worker.class);
    System.out.println(w1 == w2);
  }
}

上面的代碼中創(chuàng)建了一個BeanFactory扩然,并兩次調用getBean方法獲取Worker對象w1和w2并比較他們的引用地址是否相等。

true

從結果顯示聋伦,兩個地址是一樣的夫偶,表示兩個引用指向的是容器中同一個實例界睁。

然后我們再給Worker類加上@Scope注解并指定為prototype。

@Component("worker")
@Scope("prototype")
public class Worker {  
  public void work(Tools tools){
    tools.repair();
  }
}

再次執(zhí)行單元測試查看結果兵拢。

false

從結果得知翻斟,每次調用getBean方法時,容器都是新建了一個實例并返回说铃。

2.2 實現(xiàn)依賴注入

前面已經完成了容器的基礎功能访惜,但只有基礎功能并不能稱之為IoC容器,因此還需要給容器添加注入的能力腻扇。容器在裝配Bean的過程需要對當前Bean實例本身進行依賴檢查债热,看看它有沒有對其他對象有依賴,而整個檢查的過程是向下遞歸的幼苛。舉個例子窒篱,容器在構建A對象時,需要檢查是否依賴的了B對象舶沿,如果存在依賴墙杯,那么回到容器中查找B實例,如果此時B實例未創(chuàng)建暑椰,那么就根據(jù)定義的Scope來構建B實例(如果是單例會保存到容器中)霍转,接著對B實例進行依賴檢查,看看是否依賴的C對象一汽,如果存在依賴,那么又回到容器中查找C低滩,以此一直往下遞歸檢查召夹,最后將C對象賦值到B對象中,再把B對象賦值到A對象中恕沫,直到完成所有對象的依賴裝配监憎。

2.2.1 注入的形式

既然依賴注入是容器對實例檢查并賦值的一個過程,那么我們在開發(fā)的過程中需要告知容器可以通過哪些形式來注入婶溯。但不管哪種形式鲸阔,目的都是為容器提供一個賦值的入口,容器會在裝配Bean實例的時候通過這些入口將需要注入的對象傳入進來迄委。常見的注入形式分為以下幾種:

  • 構造方法注入
  • set方法注入
  • 接口注入
  • 字段注入

為了簡單起見凡怎,文章中只實現(xiàn)基于注解的set方法注入和字段注入兩種方式聪姿。

2.2.2 定義@Inject注解

如果注入的形式有多種,怎么告訴容器使用哪種方式注入呢?其實我們同樣可以使用一個自定義的注解來進行配置井氢,例如下面的代碼。

@Component("worker")
public class Worker {
  
  private Tools tools;
  
  @Inject("wrench")
  public void setTools(Tools tools){
    this.tools = tools;
  }  
}

當容器解析到set方法上有@Inject注解時,表示通過調用set方法來注入一個在容器中標識為wrench的Bean實例。當然残吩,這個注解同樣可以標注在字段上,那么解析到此字段的時候通過容器查找標識為wrench的Bean實例并賦值給當前的字段倘核。

@Component("worker")
public class Worker {
  
  @Inject("wrench")
  private Tools tools;
  
  public void setTools(Tools tools){
    this.tools = tools;
  }  
}

那么下面我們先自定義@Inject的注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Inject {
    String name();
}

@Target({ElementType.FIELD, ElementType.METHOD})表示此注解可以同時標注在字段和方法上泣侮。

2.2.3 定義抽象注入處理器

為什么需要抽象的注入接口?因為字段注入和set方法注入的實現(xiàn)過程是不一樣(將來可能還會擴展構造方法注入)紧唱,因此這里定義一個抽象的注入器InjectHandler旁瘫。代碼如下。

public interface InjectHandler {
    // 抽象注入行為,便于不同的注入實現(xiàn),例如字段注入或方法注入
    void handle(Object target, Class<?> targetClass, BeanFactory factory);
}

handle方法的第一個參數(shù)是被注入對象的實例琼蚯,例如A需要注入一個B酬凳,而target指的就是A的實例。第二個參數(shù)是被注入對象的Class對象遭庶,也就是A的Class宁仔。第三個參數(shù)是Bean容器的實例。這個參數(shù)非常關鍵峦睡,因為當A需要注入B的時候翎苫,那么需要從容器中獲取B實例,這是注入非常關鍵的一步榨了。

2.2.4 實現(xiàn)set方法和字段注入

接下來我們看看字段和set方法注入的具體實現(xiàn)煎谍。

  • 實現(xiàn)字段注入
public class FieldInjectHandler implements InjectHandler {

    public void handle(Object target, Class<?> targetClass, BeanFactory factory) {
        // 遍歷當前類中的字段
        for (Field field : targetClass.getDeclaredFields()) {
            // 判斷字段是否定義了@Inject注解類型
            if (field.isAnnotationPresent(Inject.class)) {
                // 獲取該屬性上的Inject注解
                Inject annotation = field.getAnnotation(Inject.class);
                // 根據(jù)注解name屬性的值,從容器獲取bean實例
                Object property = factory.getBean(annotation.name());
                // 給當前的field屬性賦值(注入)
                injectField(field, target, property);
            }
        }
    }

    private void injectField(Field field, Object target, Object property) {
        try {
            //打開訪問開關
            if(!field.isAccessible()) {
                field.setAccessible(true);
            }
            //給字段賦值
            field.set(target, property);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Field inject fail.", e);
        }
    }
}
  • 實現(xiàn)set方法注入
public class MethodInjectHandler implements InjectHandler {
    
    public void handle(Object target, Class<?> targetClass, BeanFactory factory) {
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(targetClass,
                    Object.class);
            PropertyDescriptor[] propertyDescriptors = beanInfo
                    .getPropertyDescriptors();
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                targetClass.getDeclaredField(propertyDescriptor.getName());
                //獲取屬性描述符的set方法
                Method setMethod = propertyDescriptor.getWriteMethod();
                //判斷set方法上是否標注了@Inject注解
                if (setMethod != null && setMethod.isAnnotationPresent(Inject.class)) {
                    // 獲取該方法上的Inject注解
                    Inject annotation = setMethod.getAnnotation(Inject.class);
                    // 根據(jù)注解name屬性的值,從容器獲取bean實例
                    Object property = factory.getBean(annotation.name());
                    // 回調set方法將property注入
                    setMethod.invoke(target, property);
                }
            }
        } catch (Exception e) {
            new RuntimeException("Set method inject fail.", e);
        }
    }
}

2.2.5 編寫InjectHandlerInvoker類

在一個類中可能同時出現(xiàn)一個或多個Field注入和set方法注入的情況,例如下面代碼片段中wrench是通過Field注入的龙屉,而pliers則是通過set方法注入呐粘。

@Component("worker")
public class Worker {
  
  @Inject("wrench")
  private Tools wrench;
  private Tools pliers;
  
  @Inject("pliers")
  public void setPliers(Tools pliers){
    this.pliers = pliers;
  }  
}

如何同時滿足這些不同的注入方式呢?因此在InjectHandlerInvoker類中可以維護所有的注入實現(xiàn)類转捕,并統(tǒng)一批量調用所有的InjectHandler實現(xiàn)來完成不同形式的注入作岖。

public class InjectHandlerInvoker {
    
    private static List<InjectHandler> handlers = new ArrayList<>();

    /**
     * 初始化注入處理器
     */
    static {
        handlers.add(new FieldInjectHandler());
        handlers.add(new MethodInjectHandler());
    }

    /**
     * 執(zhí)行注入操作
     * @param bean 被注入的bean實例
     * @param targetClass 被注入的Bean的class
     * @param factory 容器
     * @return
     */
    public static Object inject(Object bean, Class<?> targetClass,BeanFactory factory) {
        for(InjectHandler handler : handlers){
            handler.handle(bean, targetClass, factory);
        }
        return bean;
    }
}

2.2.6 將注入功能合并到容器中

實現(xiàn)了依賴注入的基本功能之后,最后一步就是要將注入功能集成到之前編寫好的容器中五芝。這里我們首先要考思考一個問題痘儡,容器應該在什么時候對Bean實例進行依賴裝配。在前面編寫的BeanFactory的代碼中我們得知容器在構建Bean實例時分兩種形式枢步,一種是以singleton的方式創(chuàng)建沉删,并且在創(chuàng)建容器時一并將所有的單例構建完成并放入容器中。第二種是以prototype的方式創(chuàng)建醉途,并且是在調用getBean的時候才進行構建矾瑰。因此針對這兩種方式我們可以將依賴注入的動作分別在不同的構建周期中來進行。

2.2.7 改造BeanFactory

對于singleton的情況结蟋,可以在BefanFactory執(zhí)行initSingleton方法之后就對所有單例進行依賴裝配脯倚。修改BeanFactory新增assemblySingletons方法,代碼如下:

/**
 * 為初始化的singleton實例執(zhí)行依賴注入
 */
private void assemblySingletons() {
    for (String beanName : singletonMap.keySet()) {
        Class<?> beanClass = definitionMap.get(beanName).getBeanClass();
        Object bean = singletonMap.get(beanName);
        InjectHandlerInvoker.inject(bean, beanClass, this);
    }
}

接著在BeanFactory構造方法中調用assemblySingletons方法,代碼如下:

public BeanFactory(String path) {
    Set<String> classNames = ScanUtil.scan(path);
    //初始化原型
    initDefinitionMap(classNames);
    //初始化單例
    initSingleton();
    //執(zhí)行singleton實例裝配
    assemblySingletons();
}

對于prototype的情況,可以在調用doGetBean方法時進行依賴裝配推正。修改BeanFactory新增assemblyPrototype方法恍涂,代碼如下:

/**
 * 為prototype實例執(zhí)行裝配
 */
protected Object assemblyPrototype(BeanDefinition definition){
    Object bean = newInstance(definition);
    InjectHandlerInvoker.inject(bean, definition.getBeanClass(), this);
    return bean;
}

最后修改doGetBean的方法,代碼如下:

private Object doGetBean(String beanName) {
    BeanDefinition definition = definitionMap.get(beanName);
    if("singleton".equals(definition.getScope())){
        return singletonMap.get(beanName);
    }
    return assemblyPrototype(definition);
}

注意植榕,在調用assemblySingletons或者assemblyPrototype方法時再沧,這里會產生遞歸。因為在FieldInjectHandler和MethodInjectHandler執(zhí)行注入的過程中需要調用BeanFactory的doGetBean方法從容器中查找需要注入的實例尊残,接著繼續(xù)對查找出來的對象執(zhí)行依賴檢查和裝配的過程炒瘸。

修改后的BeanFactory代碼:

public class BeanFactory {
    /**
     * 存放bean的描述
     */
    final Map<String, BeanDefinition> definitionMap = new ConcurrentHashMap<>();

    /**
     * 存放單例bean的實例
     */
    final Map<String, Object> singletonMap = new ConcurrentHashMap<>();

    /**
     * 在構造方法中初始化并構建所有bean描述
     * 以及單例的bean
     *
     * @param path 掃描路徑
     */
    public BeanFactory(String path) {
        Set<String> classNames = ScanUtil.scan(path);
        //初始化原型
        initDefinitionMap(classNames);
        //初始化單例
        initSingleton();
        //執(zhí)行singleton實例裝配
        assemblySingletons();
    }

    /**
     * 根據(jù)掃描的類名進行解析,找出帶有@Component注解的類寝衫,并構建成
     * BeanDefinition實例顷扩,保存到definitionMap集合中
     */
    private void initDefinitionMap(Set<String> classNames) {
        for (String className : classNames) {
            Class<?> beanClass = getClass(className);
            //檢查beanClass是否標注了@Component注解
            if (beanClass.isAnnotationPresent(Component.class)) {
                //獲取@Component注解的value屬性的值,這個值作為bean在容器的唯一標識
                String beanName = beanClass.getAnnotation(Component.class).value();
                //如果容器已經存在bean慰毅,則拋出異常
                if (definitionMap.containsKey(beanName)) {
                    throw new RuntimeException(
                            "conflicts with existing, non-compatible bean definition of same name and class ["
                                    + beanClass + "]");
                } else {
                    definitionMap.put(beanName,
                            createBeanDefinition(beanClass));
                }
            }
        }
    }

    /**
     * 根據(jù)權限頂類名獲取Class對象
     *
     * @param className
     * @return
     */
    private Class<?> getClass(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Can not find the class name " + className + " to build the description.");
        }
    }

    /**
     * 構建bean描述定義,將bean的scope以及類名封裝到BeanDefinition中
     * 創(chuàng)建的Bean描述會放入definitionMap的集合中保存
     * Bean的類名作為集合的key,而整個BeanDefinition對象作為value
     *
     * @param beanClass
     */
    private BeanDefinition createBeanDefinition(Class<?> beanClass) {
        // 創(chuàng)建BeanDefinition
        BeanDefinition definition = new BeanDefinition();
        //設置Bean的Class對象
        definition.setBeanClass(beanClass);
        //設置Bean的作用域
        definition.setScope(resolveScope(beanClass));
        return definition;
    }

    /**
     * 解析Scope隘截,如果bean的class上指定了Scope注解,則將@Scope的value屬性值作為Bean的創(chuàng)建方式
     * 否則Bean的默認創(chuàng)建方式將使用單例
     */
    private String resolveScope(Class<?> beanClass) {
        String scope = (beanClass.isAnnotationPresent(Scope.class)) ? beanClass
                .getAnnotation(Scope.class).value() : "singleton";
        return scope;
    }

    /**
     * 初始化SINGLETON實例放入bean容器中
     */
    private void initSingleton() {
        for (String beanName : definitionMap.keySet()) {
            BeanDefinition definition = definitionMap.get(beanName);
            if ("singleton".equals(definition.getScope())) {
                Object bean = newInstance(definition);
                singletonMap.put(beanName, bean);
            }
        }
    }

    /**
     * 為所有singleton實例執(zhí)行裝配(依賴注入)
     */
    private void assemblySingletons() {
        for (String beanName : singletonMap.keySet()) {
            Class<?> beanClass = definitionMap.get(beanName).getBeanClass();
            Object bean = singletonMap.get(beanName);
            InjectHandlerInvoker.inject(bean, beanClass, this);
        }
    }

    /**
     * 為prototype實例執(zhí)行裝配
     */
    protected Object assemblyPrototype(BeanDefinition definition){
        Object bean = newInstance(definition);
        InjectHandlerInvoker.inject(bean, definition.getBeanClass(), this);
        return bean;
    }

    /**
     * 根據(jù)描述定義創(chuàng)建Bean實例
     * @param definition
     * @return
     */
    private Object newInstance(BeanDefinition definition) {
        try {
            return definition.getBeanClass().newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Create bean instance fail.", e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Create bean instance fail.", e);
        }
    }

    /**
     * 獲取bean實例
     *
     * @param beanName
     * @return
     */
    public Object getBean(String beanName) {
        return doGetBean(beanName);
    }

    /**
     * 獲取bean實例(泛型)
     *
     * @param beanName
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T getBean(String beanName, Class<T> clazz) {
        return (T) doGetBean(beanName);
    }

    /**
     * 從容器中獲取Bean的BeanDefinition
     * 如果Bean的BeanDefinition的scope為singleton,則從singletonMap中獲取單例
     * 否則裝配原型并返回
     */
    private Object doGetBean(String beanName) {
        BeanDefinition definition = definitionMap.get(beanName);
        if("singleton".equals(definition.getScope())){
            return singletonMap.get(beanName);
        }
        return assemblyPrototype(definition);
    }
}

2.2.8 綜合測試

public abstract class Tools {
  
    public abstract void repair();
}
@Component("wrench")
public class Wrench extends Tools{
  
    @Override
    public void repair() {
        System.out.println("Use wrench repairing...");
    }
}
@Component("pliers")
public class Pliers extends Tools {

    @Override
    public void repair() {
        System.out.println("Use pliers repairing...");
    }
}
@Component("worker")
public class Worker {

    /**
     * 字段注入
     */
    @Inject(name = "wrench")
    private Tools wrench;

    private Tools pliers;

    /**
     * set方法注入
     */
    @Inject(name = "pliers")
    public void setPliers(Tools pliers) {
        this.pliers = pliers;
    }

    public void useWrench(){
        wrench.repair();
    }

    public void usePliers(){
        pliers.repair();
    }
}

測試:

public class Main {

    public static void main(String[] args) {
        BeanFactory beanFactory = new BeanFactory("edu.demo");
        Worker worker = beanFactory.getBean("worker", Worker.class);
        worker.useWrench();
        worker.usePliers();
    }
}

運行結果:

Use wrench repairing...
Use pliers repairing...

3. 結束語

本文只實現(xiàn)了一個極度簡化版本的IoC容器,并不適用于生產環(huán)境汹胃,其目的是為了清楚了解IoC的核心機制以及實現(xiàn)思路婶芭。如果希望有更深層次的了解,建議各位讀者可以閱讀spring-framework源碼着饥,里面會有更多你想要的答案犀农。

附源碼地址:https://github.com/sea-coders/beans

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市宰掉,隨后出現(xiàn)的幾起案子呵哨,更是在濱河造成了極大的恐慌,老刑警劉巖贵扰,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仇穗,死亡現(xiàn)場離奇詭異,居然都是意外死亡戚绕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門枝冀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舞丛,“玉大人,你說我怎么就攤上這事果漾∏蚯校” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵绒障,是天一觀的道長吨凑。 經常有香客問我,道長,這世上最難降的妖魔是什么鸵钝? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任糙臼,我火速辦了婚禮,結果婚禮上恩商,老公的妹妹穿的比我還像新娘变逃。我一直安慰自己,他們只是感情好怠堪,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布揽乱。 她就那樣靜靜地躺著,像睡著了一般粟矿。 火紅的嫁衣襯著肌膚如雪凰棉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天陌粹,我揣著相機與錄音撒犀,去河邊找鬼。 笑死申屹,一個胖子當著我的面吹牛绘证,可吹牛的內容都是我干的。 我是一名探鬼主播哗讥,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼嚷那,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了杆煞?” 一聲冷哼從身側響起魏宽,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎决乎,沒想到半個月后队询,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡构诚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年蚌斩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片范嘱。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡送膳,死狀恐怖,靈堂內的尸體忽然破棺而出丑蛤,到底是詐尸還是另有隱情叠聋,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布受裹,位于F島的核電站碌补,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜厦章,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一镇匀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闷袒,春花似錦坑律、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至也物,卻和暖如春宫屠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背滑蚯。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工浪蹂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人告材。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓坤次,卻偏偏與公主長得像,于是被迫代替她去往敵國和親斥赋。 傳聞我的和親對象是個殘疾皇子缰猴,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內容