对于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. 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”注释的成员函数。