重讀IoC:從DI到CDI (下)

實例三:使用setter injection的MovieLister

這裏的例子可以從github獲得(branch:movieLister_setter_DI):



git://github.com/subaochen/movieLister.git



1

git://github.com/subaochen/movieLister.git



或者直接下載ZIP壓縮包:https://github.com/subaochen/movieLister/archive/movieLister_setter_DI.zip

區別與構造方法注入,setter依賴注入首先使用默認的構造方法創建組件對象,然後調用一些setter方法設置組件的相關屬性。在具體的container實現中,至少需要處理兩種類型的setter方法:

  1. 一種setter方法是聲明瞭本組件和其他組件的關係,比如MovieLister中的setMovieFinder方法;

  2. 一種setter方法是爲了設置本組件的相關屬性,比如ColonDelimitedMovieFinder中的setMovieFile方法, 這就需要一個配置文件明確setter方法的參數。比如本例的ColonDelimitedMovieFinder,電影庫文件名稱需要通過解析配置文件 獲得。



public class ColonDelimitedMovieFinder implements MovieFinder{    private String movieFile;    public String getMovieFile() {        return movieFile;    }    public void setMovieFile(String movieFile) {        this.movieFile = movieFile;    }    ...... }



1

2

3

4

5

6

7

8

9

10

11

12

public class ColonDelimitedMovieFinder implements MovieFinder{

    private String movieFile;

 

    public String getMovieFile() {

        return movieFile;

    }

 

    public void setMovieFile(String movieFile) {

        this.movieFile = movieFile;

    }

    ......

}



配置文件beans.xml內容如下:






1




在container中,我們看到在注入組件時使用默認的構造方法創建組件對象,然後逐個分析組件的setter方法並執行之。當然,這裏對setter方法的分析是粗線條的,只是一個示例:



public class MyContainer implements Container {    private Map<Class, Object> compMap = new HashMap<Class, Object>(0);    @Override    public void registerComponent(Class compKey, Class compImplementation, Object[] parameters) {        if (compMap.get(compKey) != null) {            return;        }        Constructor[] constructors = compImplementation.getConstructors();        try {            // 這裏只支持一個默認的構造方法            Constructor constructor = constructors[0];            Object comp = constructor.newInstance(null);            Method[] methods = compImplementation.getDeclaredMethods();            if (methods != null && methods.length != 0) {                for (Method method : methods) {                    if (method.getName().startsWith("set")) {                        Object result = verifyAndInvoke(comp, method);                        if (result != null) {                            comp = result;                        }                    }                }            }            compMap.put(compKey, comp);        } catch (InstantiationException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (IllegalAccessException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (IllegalArgumentException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (InvocationTargetException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        }    }    @Override    public void registerComponent(Class clazz) {        registerComponent(clazz, clazz, null);    }    @Override    public Object getComponent(Class clazz) {        // TODO Auto-generated method stub        return compMap.get(clazz);    }    private boolean hasNullArgs(Object[] args) {        for (int i = 0; i < args.length; i++) {            Object arg = args[i];            if (arg == null) {                return true;            }        }        return false;    }    private Object getComponentForParam(Class param) {        for (Iterator iterator = compMap.entrySet().iterator(); iterator.hasNext();) {            Map.Entry entry = (Map.Entry) iterator.next();            Class clazz = (Class) entry.getKey();            if (param.isAssignableFrom(clazz)) {                return entry.getValue();            }        }        return null;    }    private Object verifyAndInvoke(Object comp, Method method) {        // 如果此方法是在配置文件中已經配置過了的屬性,則採用屬性文件beans.xml中的設置調用setter方法        String methodName = method.getName();        String propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);        String propertyValue = parseBeansConfig(comp.getClass().getName(), propertyName);        Object arg = null;        if (propertyValue != null) {            arg = propertyValue;        } else {            Class[] params = method.getParameterTypes();            if (params == null || params.length != 1) {                return null;            }            arg = getComponentForParam(params[0]);        }        if (arg == null) {            return null;        }        try {            method.invoke(comp, arg);            return comp;        } catch (IllegalAccessException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (IllegalArgumentException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (InvocationTargetException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        }        return null;    }    private String parseBeansConfig(String className, String propertyName) {        try {            InputStream is = getClass().getResourceAsStream("beans.xml");            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();            Document doc = dBuilder.parse(is);            doc.getDocumentElement().normalize();            NodeList nList = doc.getElementsByTagName("bean");            for (int temp = 0; temp < nList.getLength(); temp++) {                Node nNode = nList.item(temp);                if (nNode.getNodeType() == Node.ELEMENT_NODE) {                    Element element = (Element) nNode;                    // 只需要檢查指定的class屬性                    String clazzName = element.getAttribute("class");                    if(clazzName.equals(className)) {                        NodeList inList = nNode.getChildNodes();                        for(int i = 0; i < inList.getLength(); i++){                            Node inNode = inList.item(i);                            if(inNode.getNodeType() == Node.ELEMENT_NODE){                                Element inElement = (Element)inNode;                                String pName = inElement.getAttribute("name");                                // 只檢查指定的propertyName                                if(pName.equalsIgnoreCase(propertyName))                                    return inElement.getElementsByTagName("value").item(0).getTextContent();                            }                        }                    }                                    }            }        } catch (Exception e) {            e.printStackTrace();        }        return null;    } }



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

public class MyContainer implements Container {

 

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

 

    @Override

    public void registerComponent(Class compKey, Class compImplementation, Object[] parameters) {

        if (compMap.get(compKey) != null) {

            return;

        }

 

        Constructor[] constructors = compImplementation.getConstructors();

        try {

            // 這裏只支持一個默認的構造方法

            Constructor constructor = constructors[0];

            Object comp = constructor.newInstance(null);

 

            Method[] methods = compImplementation.getDeclaredMethods();

            if (methods != null && methods.length != 0) {

                for (Method method : methods) {

                    if (method.getName().startsWith("set")) {

                        Object result = verifyAndInvoke(comp, method);

                        if (result != null) {

                            comp = result;

                        }

                    }

                }

            }

            compMap.put(compKey, comp);

        } catch (InstantiationException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (IllegalAccessException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (IllegalArgumentException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (InvocationTargetException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        }

 

    }

 

    @Override

    public void registerComponent(Class clazz) {

        registerComponent(clazz, clazz, null);

    }

 

    @Override

    public Object getComponent(Class clazz) {

        // TODO Auto-generated method stub

        return compMap.get(clazz);

    }

 

    private boolean hasNullArgs(Object[] args) {

        for (int i = 0; i < args.length; i++) {

            Object arg = args[i];

            if (arg == null) {

                return true;

            }

        }

        return false;

    }

 

    private Object getComponentForParam(Class param) {

        for (Iterator iterator = compMap.entrySet().iterator(); iterator.hasNext();) {

            Map.Entry entry = (Map.Entry) iterator.next();

            Class clazz = (Class) entry.getKey();

            if (param.isAssignableFrom(clazz)) {

                return entry.getValue();

            }

 

        }

        return null;

    }

 

    private Object verifyAndInvoke(Object comp, Method method) {

        // 如果此方法是在配置文件中已經配置過了的屬性,則採用屬性文件beans.xml中的設置調用setter方法

        String methodName = method.getName();

        String propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);

        String propertyValue = parseBeansConfig(comp.getClass().getName(), propertyName);

 

        Object arg = null;

        if (propertyValue != null) {

            arg = propertyValue;

        } else {

            Class[] params = method.getParameterTypes();

            if (params == null || params.length != 1) {

                return null;

            }

            arg = getComponentForParam(params[0]);

        }

 

        if (arg == null) {

            return null;

        }

 

        try {

            method.invoke(comp, arg);

            return comp;

        } catch (IllegalAccessException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (IllegalArgumentException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (InvocationTargetException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        }

 

        return null;

    }

 

    private String parseBeansConfig(String className, String propertyName) {

        try {

            InputStream is = getClass().getResourceAsStream("beans.xml");

            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();

            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();

            Document doc = dBuilder.parse(is);

            doc.getDocumentElement().normalize();

 

            NodeList nList = doc.getElementsByTagName("bean");

 

            for (int temp = 0; temp < nList.getLength(); temp++) {

                Node nNode = nList.item(temp);

 

                if (nNode.getNodeType() == Node.ELEMENT_NODE) {

                    Element element = (Element) nNode;

                    // 只需要檢查指定的class屬性

                    String clazzName = element.getAttribute("class");

                    if(clazzName.equals(className)) {

                        NodeList inList = nNode.getChildNodes();

                        for(int i = 0; i < inList.getLength(); i++){

                            Node inNode = inList.item(i);

                            if(inNode.getNodeType() == Node.ELEMENT_NODE){

                                Element inElement = (Element)inNode;

                                String pName = inElement.getAttribute("name");

                                // 只檢查指定的propertyName

                                if(pName.equalsIgnoreCase(propertyName))

                                    return inElement.getElementsByTagName("value").item(0).getTextContent();

                            }

 

                        }

                    }                    

                }

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

 

        return null;

    }

}



MovieLister同樣使用setter方法注入MovieFinder:



public class MovieLister {    private MovieFinder finder;    public MovieFinder getFinder() {        return finder;    }    public void setFinder(MovieFinder finder) {        this.finder = finder;    }    ...... }



1

2

3

4

5

6

7

8

9

10

11

12

public class MovieLister {

    private MovieFinder finder;

 

    public MovieFinder getFinder() {

        return finder;

    }

 

    public void setFinder(MovieFinder finder) {

        this.finder = finder;

    }

    ......

}



 

實例四:使用註解實現依賴注入的MovieLister

這裏的例子可以從github獲得(branch:movieLister_annotation_DI):



git://github.com/subaochen/movieLister.git



1

git://github.com/subaochen/movieLister.git



或者直接下載ZIP壓縮包:https://github.com/subaochen/movieLister/archive/movieLister_annotation_DI.zip

註解的一個主要目的是減少對XML文件的依賴。還是MovieFinder的話題,在這裏我們一直使用 ColonDelimitedMovieFinder作爲MovieFinder的一個具體實現,但是如果增加其他的實現呢?比如 SqliteMovieFinder,把電影庫放到數據庫中,在MovieLister中如何靈活的指定具體使用哪個MovieFinder的實現呢?我 們看一下註解是如何做到這一點的:



public class MovieLister {    @Inject    MovieFinder finder;    .... }



1

2

3

4

5

public class MovieLister {

    @Inject

    MovieFinder finder;

    ....

}



在 MovieLister中我們通過@Inject註解告訴容器,這裏需要一個MovieFinder類型的組件,請容器注入到MovieLister組件(即當前組件)中。那麼Container該如何處理呢:



public void registerComponent(Class compKey, Class compImplementation, Object[] parameters) {        if (compMap.get(compKey) != null) {            return;        }        Constructor[] constructors = compImplementation.getConstructors();        try {            // 這裏只支持一個默認的構造方法            Constructor constructor = constructors[0];            Object comp = constructor.newInstance(null);            // 處理註解Inject            Field[] fields = compImplementation.getDeclaredFields();            for(Field field:fields){                Inject inject = field.getAnnotation(Inject.class);                if(inject != null) {                    Class clazz = field.getType();                    Object arg = getComponentForParam(clazz);                    field.set(comp, arg);                }            }            compMap.put(compKey, comp);        } catch (InstantiationException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (IllegalAccessException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (IllegalArgumentException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        } catch (InvocationTargetException ex) {            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);        }    }



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

    public void registerComponent(Class compKey, Class compImplementation, Object[] parameters) {

        if (compMap.get(compKey) != null) {

            return;

        }

 

        Constructor[] constructors = compImplementation.getConstructors();

        try {

            // 這裏只支持一個默認的構造方法

            Constructor constructor = constructors[0];

            Object comp = constructor.newInstance(null);

 

            // 處理註解Inject

            Field[] fields = compImplementation.getDeclaredFields();

            for(Field field:fields){

                Inject inject = field.getAnnotation(Inject.class);

                if(inject != null) {

                    Class clazz = field.getType();

                    Object arg = getComponentForParam(clazz);

                    field.set(comp, arg);

                }

            }

 

            compMap.put(compKey, comp);

        } catch (InstantiationException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (IllegalAccessException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (IllegalArgumentException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        } catch (InvocationTargetException ex) {

            Logger.getLogger(MyContainer.class.getName()).log(Level.SEVERE, null, ex);

        }

    }



在Container中,檢查每個Field是否存在註解Inject。如果存在的話,則根據這個Inejct註解過了的Field的類型查找已經 註冊過的組件並賦值進來即可,這就是使用註解依賴注入的全部祕密了。可以看出,在MovieLister中我們只需要告訴容器需要注入的組件的索引,置於 最終注入的組件對象是什麼類型的,取決於這個組件是如何註冊的:還記得在容器中是通過一個Map保存註冊的組件的嗎?

最後是Inject註解的實現:



@Target(ElementType.FIELD)   @Retention(RetentionPolicy.RUNTIME)   @Documented   public @interface Inject { }



1

2

3

4

5

@Target(ElementType.FIELD)  

@Retention(RetentionPolicy.RUNTIME)  

@Documented  

public @interface Inject {

}



在Client中註冊和調用組件:



public class Client {    public static void main(String[] args){        Container container = configureContainer();        MovieLister lister = (MovieLister)container.getComponent(MovieLister.class);        List movies = lister.moviesDirectedBy("zhang yi mou");        for(Movie movie:movies)            System.out.println(movie.getTitle());    }    public static Container configureContainer(){        Container container = new MyContainer();        container.registerComponent(MovieFinder.class, SqliteMovieFinder.class, null);        container.registerComponent(MovieLister.class);        return container;    } }



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class Client {

    public static void main(String[] args){

        Container container = configureContainer();

        MovieLister lister = (MovieLister)container.getComponent(MovieLister.class);

        List movies = lister.moviesDirectedBy("zhang yi mou");

 

        for(Movie movie:movies)

            System.out.println(movie.getTitle());

    }

 

    public static Container configureContainer(){

        Container container = new MyContainer();

        container.registerComponent(MovieFinder.class, SqliteMovieFinder.class, null);

        container.registerComponent(MovieLister.class);

        return container;

    }

}



注意到,在configureContainer中,如果我們需要ColonDelimitedMovieFinder,則 registerComponent(MovieFinder.class,ColonDelimitedMovieFinder.class, null);如果我們需要SqliteMovieFinder,則registerComponent(MovieFinder.class, SqliteMovieFinder.class, null)。這裏實現的簡易Container還沒有辦法通過配置的方式決定使用哪個MovieFinder的具體實現,注意和CDI中的方式區別開來。
注意,這裏的Inject註解是我們自己實現的,不是CDI中的Inject註解,不要混淆了。

Constructor Injection or Setter Injection or Annotation Injection?

我們重點看一下MovieLister組件在三種不同的依賴注入方式時的差別。

Constructor InjectionSetter InjectionAnnotation Injection


public class MovieLister { MovieFinder finder; public MovieLister(MovieFinder finder){ this.finder = finder; } }



1

2

3

4

5

6

7

8

public class MovieLister {

MovieFinder finder;

 

public MovieLister(MovieFinder finder){

this.finder = finder;

}

 

}




public class MovieLister {    private MovieFinder finder;    public MovieFinder getFinder() {        return finder;    }    public void setFinder(MovieFinder finder) {        this.finder = finder;    } }



1

2

3

4

5

6

7

8

9

10

11

public class MovieLister {

    private MovieFinder finder;

 

    public MovieFinder getFinder() {

        return finder;

    }

 

    public void setFinder(MovieFinder finder) {

        this.finder = finder;

    }

}




public class MovieLister {    @Inject    MovieFinder finder; }



1

2

3

4

5

public class MovieLister {

 

    @Inject

    MovieFinder finder;

}



MovieLister組件依賴於MoveFinder組件,也就是說,在創建MovieLister之前需要首先創建MovieFinder組 件,以便在創建MovieLister組件中設置finder屬性(注意到三種依賴注入方法中MovieLister都有一個MovieFinder類型 的屬性finder)。三種不同的依賴注入方法的本質差別在於如何設置finder屬性:

  • constrcutor依賴注入是在構造方法中設置finder屬性,這樣創建MovieLister對象時就初始化了finder屬性。

  • setter依賴注入是在setter方法中設置finder屬性,因此創建MovieLister對象時並沒有初始化finder屬性,而是通 過調用setFinder方法正確設置了finder屬性。需要注意到的是,容器自動調用setFinder方法並自動查找MovieFinder的一個 合適實現(這裏是ColonDelimitedMovieFinder)賦值給finder屬性的。

  • annotation依賴注入也是首先創建MovieLister對象,然後對於通過@Inject註解了的屬性finder進行依賴注入的處 理,初始化finder屬性。需要注意的是,容器自動查找MovieFinder的一個合適實現(這裏是 ColonDelimitedMovieFinder)並賦值給finder屬性的。

對於Constructor Inject和setter Injection的選擇,很認同Martin Fowler的觀點:當組件的規模不大的時候,儘量使用Constructor Injection,因爲只要提供幾個不同的Constructor,可以一目瞭然的看清楚在什麼情況下使用哪個Constructor以及組件之間的依 賴關係。但是當組件的規模變大,constructor變多,尤其是constructor的參數變多的時候,組件之間的依賴關係和順序就變得難以把握或 者很容易出錯了,最好轉向setter Injection。通過使用setter Injection,實際是把複雜的組件構建工作分而制之,只要認真的寫好每個setter方法,整個組件的構建就是水到渠成的事情了。

Martin Fowler並沒有闡述Annotation Injection,因爲Martin Fowler寫那篇文章的時候Annotation技術還沒有正式發佈(JDK 1.5是在2004年10月發佈的,Martin Fowler的文章是在2004年1月23日發佈的)。Annotation Injection的實現機理和Setter Injection非常相似,但是Annotation Injection比setter Injection更加方便和靈活:

  • 有時候我們需要隱藏一些setter method(什麼情況下?這裏最好舉幾個例子),setter Injection模式只能通過訪問限制來告訴容器是否啓用這個setter method,比如將setter方法設置爲private(通常setter method是public的)避免容器自動解析setter method。Annotation Inject模式就更加靈活一些了:容器只會解析註解過了的屬性或者方法,對於沒有註解過的屬性或者方法不予理會。

  • Annotation Injection更加明確和直接:通過在屬性或者方法上面使用特定的註解(比如本例的@Inject)明確告訴容器這裏需要注入一個怎樣的組件。

使用Annotation Injection的唯一負擔是程序員需要掌握相關的註解:只要理解了註解在依賴注入中實現原理(比如本例中@Inject),掌握這樣的幾個註解就是輕而易舉的事情了。

實例五:使用Service Locator的MovieLister

這裏的例子可以從github獲得(branch:movieLister_service_locator):



git://github.com/subaochen/movieLister.git



1

git://github.com/subaochen/movieLister.git



或者直接下載ZIP壓縮包:https://github.com/subaochen/movieLister/archive/movieLister_service_locator.zip

DI主要解決了組件之間的依賴關係,提高了組件的獨立性,即實現了組件的鬆耦合。比如MovieLister依賴於MovieFinder,但是 MovieFinder在不同的場合、不同的應用程序中可以有不同的實現,當使用DI實現MovieLister的時候,並沒有強制綁定一個 MovieFinder的實現,只需要在不同的場合配置不同的MovieFinder實現,MovieLister的代碼無須做任何修改,這樣 MovieLister就可以封裝爲一個通用的獨立的組件。

DI不是唯一可以提高組件的鬆耦合水平的技術,Service Locator是另外一個選擇,下圖示意了ServiceLocator如何解耦組件:

service_locator

可以看出,MovieLister並不直接使用ColonDelimitedMovieFinder,只和接口MovieFinder打交道。如果 增加另外的電影庫存儲方式,比如DatabaseMovieFinder,MovieLister的代碼無須任何修改,只需要重新配置 ServiceLocator即可。

ServiceLocator的代碼如下:



public class ServiceLocator {    private static ServiceLocator soleInstance;    private MovieFinder movieFinder;    public static MovieFinder movieFinder(){        return soleInstance.movieFinder;    }    public static void load(ServiceLocator arg){        soleInstance = arg;    }    public ServiceLocator(MovieFinder movieFinder){        this.movieFinder = movieFinder;    } }



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class ServiceLocator {

    private static ServiceLocator soleInstance;

    private MovieFinder movieFinder;

 

    public static MovieFinder movieFinder(){

        return soleInstance.movieFinder;

    }

 

    public static void load(ServiceLocator arg){

        soleInstance = arg;

    }

 

    public ServiceLocator(MovieFinder movieFinder){

        this.movieFinder = movieFinder;

    }

 

}



MovieLister的代碼如下:



public class MovieLister {    private MovieFinder finder = ServiceLocator.movieFinder();    public List moviesDirectedBy(String director){        List movies = new ArrayList(0);        List allMovies = finder.findAll();        for(Movie movie:allMovies)            if(movie.getDirector().equalsIgnoreCase(director))                movies.add(movie);        return movies;    } }



1

2

3

4

5

6

7

8

9

10

11

12

13

public class MovieLister {

    private MovieFinder finder = ServiceLocator.movieFinder();

 

    public List moviesDirectedBy(String director){

        List movies = new ArrayList(0);

        List allMovies = finder.findAll();

        for(Movie movie:allMovies)

            if(movie.getDirector().equalsIgnoreCase(director))

                movies.add(movie);

 

        return movies;

    }

}



Client的代碼如下:



public class Client {    public static void main(String[] args){        configure();        MovieLister lister = new MovieLister();                List movies = lister.moviesDirectedBy("zhang yi mou");        for(Movie movie:movies)            System.out.println(movie.getTitle());    }    private static void configure() {        ServiceLocator.load(new ServiceLocator(new ColonDelimitedMovieFinder("movies.txt")));    } }



1

2

3

4

5

6

7

8

9

10

11

12

13

14

public class Client {

    public static void main(String[] args){

        configure();

        MovieLister lister = new MovieLister();        

        List movies = lister.moviesDirectedBy("zhang yi mou");

 

        for(Movie movie:movies)

            System.out.println(movie.getTitle());

    }

 

    private static void configure() {

        ServiceLocator.load(new ServiceLocator(new ColonDelimitedMovieFinder("movies.txt")));

    }

}



你也許會問,通過ServiceLocator固然解耦了MovieLister和ColonDelimitedMovieFinder,但是 MovieLister卻和ServiceLocator緊密耦合在一起了!這種拆東牆補西牆的做法有意義嗎?但是,想一下實際的MovieLister 不僅僅是要用到MovieFinder,也許還會用到MovieMaker等等其他接口的實現,這個時候ServiceLocator的作用就明顯起來 了:ServiceLocator在MovieLister及其依賴的組件之間起到一個隔離作用,這樣即使MovieLister所依賴的這些組件發生了 變化也不會影響到MovieLister。

實例六:使用CDI/Weld的MovieLister

在學習和測試本例之前,請從這裏下載CDI的參考實現Weld:http://www.seamframework.org/Weld/Downloads,建議下載Weld 2.0.1 Final版本。假設你的Weld安裝到下面的目錄:$WELD_HOME=/home/youname/devel/weld。

這裏的例子可以從github獲得(branch:movieLister_service_locator):



git://github.com/subaochen/movieLister.git



1

git://github.com/subaochen/movieLister.git



或者直接下載ZIP壓縮包:https://github.com/subaochen/movieLister/archive/movieLister_CDI.zip

運行本例的最簡單方法是進入src目錄,執行:



javac -cp $WELD_HOME/artifacts/weld/weld-se.jar:. *.java java -cp $WELD_HOME/artifacts/weld/weld-se.jar:. org.jboss.environment.se.StartMain



1

2

javac -cp $WELD_HOME/artifacts/weld/weld-se.jar:. *.java

java -cp $WELD_HOME/artifacts/weld/weld-se.jar:. org.jboss.environment.se.StartMain



執行結果如下:



[main] INFO org.jboss.weld.Version - WELD-000900 2.0.1 (Final) [main] INFO org.jboss.weld.Bootstrap - WELD-000101 Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously. [main] WARN org.jboss.weld.interceptor.util.InterceptionTypeRegistry - Class 'javax.ejb.PostActivate' not found, interception based on it is not enabled [main] WARN org.jboss.weld.interceptor.util.InterceptionTypeRegistry - Class 'javax.ejb.PrePassivate' not found, interception based on it is not enabled hong gao liang qiu ju da guan si



1

2

3

4

5

6

[main] INFO org.jboss.weld.Version - WELD-000900 2.0.1 (Final)

[main] INFO org.jboss.weld.Bootstrap - WELD-000101 Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.

[main] WARN org.jboss.weld.interceptor.util.InterceptionTypeRegistry - Class 'javax.ejb.PostActivate' not found, interception based on it is not enabled

[main] WARN org.jboss.weld.interceptor.util.InterceptionTypeRegistry - Class 'javax.ejb.PrePassivate' not found, interception based on it is not enabled

hong gao liang

qiu ju da guan si



Weld提供了一個Weld擴展,該擴展可以在Java SE環境下啓動一個Weld容器來管理組件,所有我們在Java SE下面使用Weld的所需要的類都包含在weld-se.jar中了。StartMain會啓動這個組件管理器(容器)自動掃描在classpath中發現的java類並註冊進來(注意到在CDI環境中,Client也是組件!), 在容器啓動結束後觸發事件ContainerInitialized。這樣我們的應用需要組件的時候,只需要@Inject進來即可(因爲 StartMain已經自動掃描並註冊了所有發現的類);我們的應用也只需要監聽事件ContainerInitialized,StartMain即自 動執行相應的方法。

下面比較詳細的解析這個簡單的例子。

MovieFinder幾乎沒有什麼變化,只是爲了簡化起見將movieFile屬性寫死了。MovieLister看起來更簡潔了:



public class MovieLister {    @Inject MovieFinder finder;    .... }



1

2

3

4

public class MovieLister {

    @Inject MovieFinder finder;

    ....

}



這裏使用CDI的@Inject注入了MovieFinder類型的組件finder。由於在本例中只有一個 ColonDelimitedMovieFinder是MovieFinder的唯一實現,因此不需要藉助於Qualifier來區分組件,CDI容器能 夠唯一定位MovieFinder組件。

Client也有一些變化:



public class Client {    @Inject MovieLister movieLister;    public void movieFinder(@Observes ContainerInitialized event,                    @Parameters List parameters){        List movies = movieLister.moviesDirectedBy("zhang yi mou");        for(Movie movie:movies)            System.out.println(movie.getTitle());            }     }



1

2

3

4

5

6

7

8

9

10

11

12

public class Client {

    @Inject MovieLister movieLister;

 

    public void movieFinder(@Observes ContainerInitialized event,

                    @Parameters List parameters){

        List movies = movieLister.moviesDirectedBy("zhang yi mou");

 

        for(Movie movie:movies)

            System.out.println(movie.getTitle());        

    }    

 

}



首先,在Client中通過CDI的@Inject注入了MovieLister類型的組件movieLister。其次,Client並不需要 main方法,我們只需要監聽ContainerInitialized事件即可,CDI容器會自動啓動監聽了ContainerInitialized 事件的方法的。

如果我們有多個MovieFinder的具體實現呢?比如ColonDelimitedMovieFinder和SqliteMovieFinder,CDI是如何處理的呢?

首先定義兩個Qualifiers以區分MovieFinder的兩個實現(實際開發中,可以使用一個帶參數的註解實現,比如@MovieStore(“text”)):



@Qualifier @Retention(RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER}) public @interface TextMovieStore {   } @Qualifier @Retention(RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER}) public @interface SqliteMovieStore {   }



1

2

3

4

5

6

7

8

9

10

11

@Qualifier

@Retention(RUNTIME)

@Target({TYPE, METHOD, FIELD, PARAMETER})

public @interface TextMovieStore {  

}

 

@Qualifier

@Retention(RUNTIME)

@Target({TYPE, METHOD, FIELD, PARAMETER})

public @interface SqliteMovieStore {  

}



然後分別使用上面定義的Qualifiers註解ColonDelimitedMovieFinder和SqliteMovieFinder:



@TextMovieStore public class ColonDelimitedMovieFinder implements MovieFinder{ ... } @SqliteMovieStore public class SqliteMovieFinder implements MovieFinder { ... }



1

2

3

4

5

@TextMovieStore

public class ColonDelimitedMovieFinder implements MovieFinder{ ... }

 

@SqliteMovieStore

public class SqliteMovieFinder implements MovieFinder { ... }



這樣在MovieLister中就可以注入MovieFinder的某個具體實現了:



public class MovieLister {    @Inject @TextMovieStore MovieFinder finder;    .... }



1

2

3

4

public class MovieLister {

    @Inject @TextMovieStore MovieFinder finder;

    ....

}



只要改變@TextMovieStore爲@SqliteMovieStore即表示注入SqliteMovieFinder而非ColonDelimitedMovieFinder。

增加了SqliteMovieFinder之後的例子編譯和執行的命令如下:



javac -cp $WELD_HOME/artifacts/weld/weld-se.jar:../lib/sqlitejdbc.jar *.java java -cp $WELD_HOME/artifacts/weld/weld-se.jar:../lib/sqlitejdbc.jar:. org.jboss.weld.environment.se.StartMain



1

2

javac -cp $WELD_HOME/artifacts/weld/weld-se.jar:../lib/sqlitejdbc.jar *.java

java -cp $WELD_HOME/artifacts/weld/weld-se.jar:../lib/sqlitejdbc.jar:. org.jboss.weld.environment.se.StartMain



可以看出,CDI建立了更簡潔的組件模型,我們的代碼也更簡潔了。當然,這裏只是演示了CDI的一小部分(組件的定位和注入),實際上CDI即便是在java SE環境下也提供了很多的特性(摘自Weld Reference Guide):

支持 @PostConstruct and @PreDestroy 生命週期方法,可以更好的控制組件的初始化和銷燬過程。
使用qualifiers 和 alternatives更好的控制依賴注入。
支持組件的以下三種生命週期:@Application, @Dependent and @Singleton
攔截器(Interceptors)和 修飾器(decorators)
Stereotypes
Events
支持擴展(Portable extension support)

從DI到CDI:進步有多大?

  • 在CDI中,一切classpath中的java類,無論是POJO還是EJB,都被自動識別爲組件(bean),不再需要手工註冊或者配置文件。這也是“約定重於配置”原則的具體表現。

  • 在CDI中,組件不再有“名字”而只有類型,從而告別了組件的錯誤引用,實現了安全的依賴注入。比較一下picoContainer和 Spring中註冊組件的方法:在picoContainer中,一般將組件本身或者組件的接口類作爲組件的索引,比如 pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams),其中MovieFinder.class是組件ColonMovieFinder.class的索引,是 picoContainer定位組件的依據。可見,picoContainer不會出現錯誤引用組件的現象,因爲註冊組件時如果拼寫錯誤會引致編譯錯誤。 再看SpringBean的配置,是通過xml文件完成的:<beans><bean id=”movieLister” class=”MovieLister”/></beans>,然後可以通過 context.getBean(“movieLister”)獲得movieLister組件。可以看出,如果xml配置和getBean給出的參數不 一致的話,編譯器並不能發現此類錯誤,只有在運行時纔會拋出找不到組件的異常。當組件數量很多的時候,這種因爲組件的名稱不一致造成的隱患就像給軟件埋上 了若干×××一樣。

  • 基於Qualifiers和Alternatives,CDI實現了更靈活的組件識別和定位機制。

  • 藉助於CDI,普通的POJO組件也有了生命週期方法,生命週期不再是EJB的專利。

DI的未來?

Who knows?DDI?EDI?XDI?組件的可視化設計和組裝也許是未來的發展方向。

參考資料


歡迎訪問肖海鵬老師的課程中心:http://edu.51cto.com/lecturer/user_id-10053053.html

歡迎加入肖海鵬老師技術交流羣:2641394058(QQ)



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章