实例三:使用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)