Spring理論基礎-控制反轉和依賴注入

Yuicon發表於2019-01-19

第一次瞭解到控制反轉(Inversion of Control)這個概念,是在學習Spring框架的時候。IOCAOP作為Spring的兩大特徵,自然是要去好好學學的。而依賴注入(Dependency Injection,簡稱DI)卻使得我困惑了挺久,一直想不明白他們之間的聯絡。

控制反轉

控制反轉顧名思義,就是要去反轉控制權,那麼到底是哪些控制被反轉了?在2004年 Martin fowler 大神就提出了

“哪些方面的控制被反轉了?”

這個問題,他總結出是依賴物件的獲得被反轉了。

在單一職責原則的設計下,很少有單獨一個物件就能完成的任務。大多數任務都需要複數的物件來協作完成,這樣物件與物件之間就有了依賴。一開始物件之間的依賴關係是自己解決的,需要什麼物件了就New一個出來用,控制權是在物件本身。但是這樣耦合度就非常高,可能某個物件的一點小修改就會引起連鎖反應,需要把依賴的物件一路修改過去。

如果依賴物件的獲得被反轉,具體生成什麼依賴物件和什麼時候生成都由物件之外的IOC容器來決定。物件只要在用到依賴物件的時候能獲取到就可以了,常用的方式有依賴注入和依賴查詢(Dependency Lookup)。這樣物件與物件之間的耦合就被移除到了物件之外,後續即使有依賴修改也不需要去修改原始碼了。

總結一下,控制反轉是指把物件的依賴管理從內部轉移至外部。

依賴注入

控制反轉是把物件之間的依賴關係提到外部去管理,可依賴是提到物件外面了,物件本身還是要用到依賴物件的,這時候就要用到依賴注入了。顧名思義,應用需要把物件所需要的依賴從外部注入進來。可以是通過物件的建構函式傳參注入,這種叫做構造器注入(Constructor Injection)。如果是通過JavaBean的屬性方法傳參注入,就叫做設值方法注入(Setter Injection)

不管是通過什麼方式注入的,如果是我們手動注入的話還是顯得太麻煩了。這時候就需要一個容器來幫我們實現這個功能,自動的將物件所需的依賴注入進去,這個容器就是前面提到的IOC容器了。

控制反轉和依賴注入的關係也已經清晰了,它們本質上可以說是一樣的,只是具體的關注點不同。控制反轉的關注點是控制權的轉移,而依賴注入則內含了控制反轉的意義,明確的描述了依賴物件在外部被管理然後注入到物件中。實現了依賴注入,控制也就反轉了。

例子

  • 首先是傳統的方式,耦合非常嚴重。
public class Main {

    public static void main(String[] args) {
        OrderService service = new OrderService();
        service.test();
    }

}
public class OrderService {

    private OrderDao dao = new OrderDao();

    public void test() {
        dao.doSomeThing();
    }

}
public class OrderDao {

    public void doSomeThing() {
        System.out.println("test");
    }

}
  • 接下來是沒有使用容器的方式,鬆耦合了,但是手動注入非常的麻煩。
public class Main {

    public static void main(String[] args) {
        Dao dao = new OrderDao();
        OrderService service = new OrderService(dao);
        service.test();
    }

}
public interface Dao {

    void doSomeThing();

}
public class OrderDao implements Dao {

    @Override
    public void doSomeThing() {
        System.out.println("test");
    }

}
public class OrderService {

    private Dao dao;

    public OrderService(Dao dao) {
        this.dao = dao;
    }

    public void test() {
        dao.doSomeThing();
    }

}
  • 接下來使用容器造福人類。
// 引導類要放在專案根目錄下,也就是在 src 下面
public class Main {

    public static void main(String[] args) {
        // 生成容器
        Container container = new Container(Main.class);
        // 獲取Bean
        OrderService service = container.getBean(OrderService.class);
        // 呼叫
        service.test();
    }

}
@Component
public class OrderService {

    @Autowired
    private Dao dao;

    public void test() {
        dao.doSomeThing();
    }

    public Dao getDao() {
        return dao;
    }

    public void setDao(Dao dao) {
        this.dao = dao;
    }
}
@Component
public class OrderDao implements Dao {

    @Override
    public void doSomeThing() {
        System.out.println("test");
    }

}
public interface Dao {

    void doSomeThing();

}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Component {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
public @interface Autowired {
}
public class Container {

    private List<String> classPaths = new ArrayList<>();

    private String separator;

    private Map<Class, Object> components = new HashMap<>();

    public Container(Class cls) {
        File file = new File(cls.getResource("").getFile());
        separator = file.getName();
        renderClassPaths(new File(this.getClass().getResource("").getFile()));
        make();
        di();
    }

    private void make() {
        classPaths.forEach(classPath -> {
            try {
                Class c = Class.forName(classPath);
                // 找到有 @ioc.Component 註解的類並例項化
                if (c.isAnnotationPresent(Component.class)) {
                    components.put(c, c.newInstance());
                }
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 注入依賴
     */
    private void di() {
        components.forEach((aClass, o) -> Arrays.stream(aClass.getDeclaredFields()).forEach(field -> {
            if (field.isAnnotationPresent(Autowired.class)) {
                try {
                    String methodName = "set" + field.getType().getName().substring(field.getType().getName().lastIndexOf(".") + 1);
                    Method method = aClass.getMethod(methodName, field.getType());
                    if (field.getType().isInterface()) {
                        components.keySet().forEach(aClass1 -> {
                            if (Arrays.stream(aClass1.getInterfaces()).anyMatch(aClass2 -> aClass2.equals(field.getType()))) {
                                try {
                                    method.invoke(o, components.get(aClass1));
                                } catch (IllegalAccessException | InvocationTargetException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    } else {
                        method.invoke(o, components.get(field.getType()));
                    }
                } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }));
    }

    /**
     * 該方法會得到所有的類,將類的全類名寫入到classPaths中
     *
     * @param file 包
     */
    private void renderClassPaths(File file) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            Arrays.stream(Objects.requireNonNull(files)).forEach(this::renderClassPaths);
        } else {
            if (file.getName().endsWith(".class")) {
                String classPath = file.getPath()
                        .substring(file.getPath().lastIndexOf(separator) + separator.length() + 1)
                        .replace(`\`, `.`)
                        .replace(".class", "");
                classPaths.add(classPath);
            }
        }
    }

    public <T> T getBean(Class c) {
        return (T) components.get(c);
    }

}

後記

一些概念在腦海裡總以為是清晰的,等實際用到或者是寫成文字的時候就發現有很多不理解的地方。本文的目的就是梳理下概念,做些記錄。這次自己嘗試實現了下IOC容器,一開始寫就知道自己之前的理解有問題了。好歹是寫出了個能用的版本,用來應付文章中的例子。後面可以去參考下Spring的實現,估計能學到不少東西。

我的部落格地址

參考資料

相關文章