Struts2的IoC解析

jopen 12年前
对于 IoC来说,常见的就是Spring框架的了。并且在目前Java EE开发中,使用SSH框架时,也主要依赖于Spring框架所提供的IoC功能。但Struts2框架本身也提供了IoC的功能。本人对于Spring 框架的IoC功能的实现没怎么做了解,所以也不对此发表什么见解。这里主要是对Struts2框架的IoC的使用方法和实现原理进行分析。纯属个人学习兴趣。。。。
在这其中参照了博文《Struts2源码分析-IoC容器的实现机制》,链接地址为http://blog.csdn.net/fcbayernmunchen/article/details/7686385中的相关内容。
[IoC功能的使用]
首先如何理解IoC的含义呢,现在也常称为依赖注入(Dependency Injection,DI。各种称呼,对此略有点糊涂)。核心的意思就是把对象之间的耦合关系,交由外部去管理,一般就是一个容器(Container)对象。那容器,又是如何去获得关于对象的信息的呢,这就是配置文件的作用了。在Spring中,有applicationContext.xml配置文件。在Struts2中,有struts.xml配置文件。在这些配置文件中配置好对象信息,在加载的时候,这些配置文件会被解析,然后当对象被使用的时候,就通过配置文件找到相应的类,调用相应的方法等。。。。

大致是这么个思路,直接说有点抽象,下面用一个实际的例子来说明这种用法。

struts.xml中的配置项如下所示
< bean name ="entity" type ="lynn.entity.Entity" class ="lynn.entity.Entity" scope="Scope.SINGLETON"/>
配置了一个bean,名称是entity,类型是lynn.entity.Entity,实例化对象时,具体生成的是lynn.entity.Entity这个类的对象。在这个例子中type和class的值被设置成一样的。一般来说class描述的类应该是type类的子类,或是type描述的接口的实现类。 scope指出了这个对象的生存范围,这里这个对象被设置成了一个单例形式的对象。也就是,所有通过Struts2的IoC获取的对象,都是同一个对象。

下面来看lynn.entity.Entity这个类的代码,如下所示
package lynn.entity;


public class Entity {
       
        private String name ;
        private int value ;
       
       
        public Entity(){
               name= "Lynn";
               value=23;
       }
       
        public void setName(String name){
               this.name =name;
       }
       
        public void setValue(int value){
               this.value =value;
       }
       
        public String getName(){
               return name ;
       }
        public int getValue(){
               return value ;
       }
       
        @Override
        public String toString(){
               return "Entity info:<" +name +"," +value +">" ;
       }
}
lynn.entity.Entity这个类没有什么特别之处,就是一个一般的Java Bean,只不过重写了toString方法,这样就可以直接将Entity对象的相关信息打印出来。

还有一个测试inject方法的类,叫做InjectTest类,这个类的代码如下所示
package lynn.inject;

import lynn.entity.Entity;
import com.opensymphony.xwork2.inject.Inject;


public class InjectTest {
        @Inject( "entity")
        private Entity entity ;
       
       
        @Inject
        public void injectTest(@Inject("entity")Entity entity){
              System. out.println(entity);
       }
       
        public void normalMethod(){
              System. out.println("normal method" );
       }
}
主要注意这个类中加入的@Inject注释。这里指定的名称是“entity”,也就是前面在struts.xml中配置的那个java bean。

在测试项目中与lynn.entity.Entity和IoC相关的部分的代码如下所示
Container container=ServletActionContext.getContext().getContainer();
InjectTest inject= new InjectTest();
Entity entity=container.getInstance(Entity. class, "entity" );
System. out.println("first mark: " +entity);
entity.setName( "yanlinwang");
entity.setValue(50);
container.inject(inject);
Entity entity2=container.getInstance(Entity. class,"entity" );
System. out.println("second mark:" +entity2);
在这段代码中,首先通过ServletActionContext.getContext().getContainer()获得用来管理对象关系的容器(Container)对象container,然后与IoC功能相关部分的代码是container.getInstance(Entity. class "entity" );和container.inject(inject); 这里对getInstance对象调用了两次,并且在中间还对获取过的对象进行过修改,通过对两次获取到的对象的打印结果,来确定是不是像前面所说的那样,这个对象是一个单例对象。

项目运行后,与此部分相关的显示结果如下图所示

从运行结果来看,总共对这个对象打印了三次。出了在上面标出的"first mark"和"second mark"外,还进行了一次打印动作。是在InjectTest类的injectTest函数中。第一次的打印结果是<Lynn,23>,这表明是调用了默认构造函数。在后续对这个entity对象的属性值进行了设定,然后进行 container.inject(inject);时injectTest函数被调用,使得这个对象又被打印一次。这里显示的信息就是设置后的新值。然后再次通过容器获取这个对象,并且打印。这里打印出来的值与设置的值一样,表明这里获取的还是之前修改过的对象,所以,从这三次打印的结果来看,三次操作的都是同一个entity对象。

通过上面这个例子,也给出了IoC功能的一个形象说明。下面部分,将对Struts2的IoC的实现,进行解析。学习所致,不保证正确性!

[IoC功能的接口]
前面的例子中给出的对Struts2的IoC功能的使用,是通过一个容器(Container)对象来实现的。Container是一个接口类,它的源代码如下所示
public interface Container extends Serializable {

  /**
   * Default dependency name.
   */
  String DEFAULT_NAME = "default";

  /**
   * Injects dependencies into the fields and methods of an existing object.
   */
  void inject(Object o);

  /**
   * Creates and injects a new instance of type {@code implementation}.
   */
  <T> T inject(Class<T> implementation);

  /**
   * Gets an instance of the given dependency which was declared in
   * {@link com.opensymphony.xwork2.inject.ContainerBuilder}.
   */
  <T> T getInstance(Class<T> type, String name);

  /**
   * Convenience method.&nbsp;Equivalent to {@code getInstance(type,
   * DEFAULT_NAME)}.
   */
  <T> T getInstance(Class<T> type);
 
  /**
   * Gets a set of all registered names for the given type
   * @param type The instance type
   * @return A set of registered names or empty set if no instances are registered for that type
   */
  Set<String> getInstanceNames(Class<?> type);

  /**
   * Sets the scope strategy for the current thread.
   */
  void setScopeStrategy(Scope.Strategy scopeStrategy);

  /**
   * Removes the scope strategy for the current thread.
   */
  void removeScopeStrategy();
}

由上面的代码结合前面的例子,可以看出获取对象和注入相关的接口是getInstance和inject的重载方法。这就是IoC功能的相关外部接口。那么,这个过程是如何实现的呢?

[IoC功能相关的数据结构----容器实现类ContainerImpl的成员变量]
要了解Struts2的IoC的实现,就需要从上面相关容器接口方法的具体实现来看。在Struts2中,实现这些相关方法的类是ContainerImpl,它实现了一个具体的容器的功能。通过对这个容器实现类进行解析,就可以大致了解IoC的实现原理。
在对相关接口的实现函数进行源代码分析之前,首先说明下ContainerImpl类的一些与此相关的成员变量。
1、factories变量
final Map<Key<?>, InternalFactory<?>> factories;

factories是在生成容器对象时传递进来的参数,在之前的博文《Struts2的Builder模式》中介绍了参数的收集过程,并且对参数的作用作了一定的介绍。这里再简要说明下,构造容器对象时传递进来的参数是<Key,InternalFactory>的键值对。Key代表的是一个对象的类型、类型的名称,InternalFactory代表的是创建一个具体对象的工厂对象,在需要的时候,调用factory方法就可以生成这个对象。

factories是在ContainerImpl类的构造函数中被赋值的,就是参数传递进来的值。

2、injectors变量
final Map<Class<?>, List<Injector>> injectors =
 new ReferenceCache<Class<?>, List<Injector>>() {
      @Override
       protected List<Injector> create( Class<?> key ) {
          List<Injector> injectors = new ArrayList<Injector>();
            addInjectors(key, injectors);
                return injectors;
    }
};
injectors 实际上是一个ReferenceCache对象,就是在之前的博文《Struts2缓存解析》中分析的Struts2的一个缓存实现。这里是缓存了类和其注入器之间的关系。对ReferenceCache类的create函数的重写,主要是调用了ContainImpl类的addInjectors函数。这是实现缓存延迟加载原理中真正加载缓存对象的地方,在缓存中暂时找不到所要的对象时,就通过addInjectors来加载所需的对象。

addInjectors的代码如下
void addInjectors( Class clazz, List<Injector> injectors ) {
               if (clazz == Object.class) {
                      return;
              }
              addInjectors(clazz.getSuperclass(), injectors);//递归向上,为其父类实现注入,直到遇到Object为止
               //变量注入
              addInjectorsForFields(clazz.getDeclaredFields(), false, injectors);
               //方法注入
              addInjectorsForMethods(clazz.getDeclaredMethods(), false, injectors);
       }
简单说来addInjectors的作用就是递归向上依次实现注入的动作,实现变量注入和方法注入。

这里看不到真正执行注入的动作,继续分析。来看addInjectorsForFields的代码
void addInjectorsForFields( Field[] fields, boolean statics,List<Injector> injectors ) {
   addInjectorsForMembers(Arrays. asList(fields), statics, injectors,
                //匿名内部类,注意这个InjectorFactory对象
                new InjectorFactory<Field>() {
                  @Override
                   public Injector create( ContainerImpl container, Field field,String name )
                                     throws MissingDependencyException {
                       return new FieldInjector(container, field, name);//注意这里
                                  }
                  });
}
从代码上看,addInjectorsForFields也不是执行注入动作的地方,而是通过调用addInjectorsForMembers来实现。与此类似,对addInjectorsForMethods,也是通过调用addInjectorsInjectorsForMembers来执行注入的动作。也就是二者最终都汇集到了对一个方法的调用上,当主要的区别在于所传递的参数的不同之处。而影响最终注入动作的就是最后一个参数,InjectorFacotry对象。
区别点:
在addInjectorsForField中,这个InjectorFactory对象的create方法调用,返回的是一个FieldInjector对象;
在addInjectorsForMethods中,传递的InjectorFactory对象的create方法调用,返回的是一个MethodInjector对象。

下面来研究addInjectorsForMembers函数,
<M extends Member & AnnotatedElement> void addInjectorsForMembers(
                     List<M> members, boolean statics, List<Injector> injectors,
                     InjectorFactory<M> injectorFactory ) {
               for ( M member : members ) {
                      if (isStatic(member) == statics) {
                            Inject inject = member.getAnnotation( Inject. class);
                            if (inject != null) {
                                   try {
                                          //inject.value()!!
                                         injectors.add(injectorFactory.create( this, member, inject.value()));
                                  } catch ( MissingDependencyException e ) {
                                          if (inject.required()) {
                                                 throw new DependencyException(e);
                                         }
                                  }
                           }
                     }
              }
       }
注意在addInjectors方法中调用addInjectorsForFields和addInjectorsForMethods时,传递了一个false参数,传递到 addInjectorsForMembers时,就是这个statics参数为false。就是为其中非静态的实例变量和实例方法来实现注入。对于静态的类变量和类方法,这里不执行注入动作。关于静态的情况,这里就不做讨论了。。。
在上面的addInjectorForMembers方法中,主要看这两条语句
Inject inject = member.getAnnotation( Inject. class );首先获取这个成员(变量、方法)的注释情况,如果对这个成员有“@Inject”的注释,那么就执行这条语句injectors.add(injectorFactory.create( this , member, inject.value()));生成这个成员的注入器中,然后保存起来。

保存起来,保存到了何处?看整个调用过程,addInjectors(key, injectors);->
   addInjectorsForFields(clazz.getDeclaredFields(), false injectors);->
      addInjectorsForMembers(...,injectors,...);

其实是保存在了由传进来的参数指定的List<Injector>中,也就是设置的缓存变量ReferenceCache类型的injectors中。再回过头来看
这个ReferenceCache类型的injectors缓存,它是ReferenceCache<Class<?>,List<Injector>>,也就是通过类类型,来获取这个类的所有的注入器。

根据之前对缓存的讲解,当找不到缓存值时,就会实现加载,也就是调用create。将调用create的值返回,也就是那些生成的注入器了。具体说来,就是当通过 injectors.get(XXX.class);针对这个类类型加载的注入器会被返回。也就是前面所说的保存的地方了!

[IoC的具体实现]
与Struts2的IoC相关的数据结构介绍完之后,现在来说下接口函数的实现,同时这里以对成员变量的注入为例,通过FieldInjector来看,注入操作到底做了什么。

首先来看 getInstance的实现。
其它的重载函数不予说明,这里给出的是真正执行动作的那个getInstance的实现。其它的重载函数最终都是通过对这个函数的调用来实现的。
<T> T getInstance( Class<T> type, String name, InternalContext context ) {
              ExternalContext<?> previous = context.getExternalContext();
              Key<T> key = Key. newInstance(type, name);
              context.setExternalContext(ExternalContext. newInstance( null, key, this ));
               try {
                      InternalFactory o = getFactory(key);
                      if (o != null ) {
                            return getFactory(key).create(context);
                     } else {
                            return null ;
                     }
              } finally {
                     context.setExternalContext(previous);
              }
       }
类容不长,真正要注意的地方就是这句话return getFactory(key).create(context);通过制定的类型和名称,来获取能够生成所需实例的工厂对象,其它的语句是对内外上下文的一些设置操作(内外上下文的具体差异,我也比较模糊)。
getFactory方法的方法体中,只有一句话
<T> InternalFactory<? extends T> getFactory( Key<T> key ) {
               return (InternalFactory<T>) factories .get(key);
       }
从上面的代码来看,对getInstance的实现是非常简单的,困难的过程是在于构造容器过程中收集参数的过程,在之前的一篇博文《Struts2的Builder模式》中有简单地介绍。

再来看inject的实现。
也是忽略其它重载函数,给出执行最终动作的inject函数的代码。inject方法的重载情况稍微复杂一点,有两条路,这里对与前面的内容相关的那一条路进行介绍。整个的代码如下
void inject( Object o, InternalContext context ) {
              List<Injector> injectors = this .injectors .get(o.getClass());
               for ( Injector injector : injectors ) {
                     injector.inject(context, o);
              }
       }
就是首先获取这个对象的所有注入器,然后依次执行注入操作。至于,具体的注入动作是做哪些事情,这里看不出来,要通过对注入器的研究来了解。前面的内容中总共出现过两类注入器 FieldInjector和MethodInjector。这里以FieldInjector为例来进行分解。对这个类的核心方法进行研究,下面是 FieldInjector类的inject函数的代码。其它部分的代码与注入功能的相关性不大,主要是做一些权限上的检查和设置,这里就不与列出了。
public void inject( InternalContext context, Object o ) {
                      //change the external context
                     ExternalContext<?> previous = context.getExternalContext();
                     context.setExternalContext( externalContext );
                      try {
                            /*
                            * "factory.create()" method will create an instance of the source type,
                            * indicated by the value of the @Inject annotation
                            * set the field value
                            * */
                            field.set(o, factory.create(context));
                     } catch ( IllegalAccessException e ) {
                            throw new AssertionError(e);
                     } finally {
                           context.setExternalContext(previous);
                     }
              }
在FieldInjector的构造函数中,需要传递三个参数,
 public FieldInjector( ContainerImpl container, Field field, String name )
在构造函数中,对factory进行初始化
Key<?> key = Key. newInstance(field.getType(), name);
factory = container.getFactory(key);
注意看addInjectorsForMembers中所传递的参数,name参数对应的是inject.value(),也就是 @Inject(value="xxxx")或@Inject("")所指定的名称。一般对应于struts.xml中配置的bean的名称。在对 FieldInject例子来说,就是这个成员变量所将要真正对应的那个bean。

结合上面的代码来看,FieldInjector的inject方法的主要动作就是field.set(o, factory.create(context));设置指定对象的某个成员变量的值。这个对象o就是通过inject入口传进来的对象了。
所以,对FieldInjector的inject函数做个小结,就是设置某一对象的标有“@Inject”注释的成员变量的值。

类似地,对MethodInjector的inject函数做个小结,就是调用某一对象的表用“@Inject”注释的成员函数。

那么在进行对象注入,也就是调用inject方法的时候,是设置该对象的加标注的成员变量的值,以及调用加标注的成员函数。

来自:http://blog.csdn.net/yanlinwang/article/details/8944632