一、容器的引入
在面向对象的世界里,业务逻辑的实现通常是由多个对象相互协作完成的,这使得每个对象都需要,与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么如你所见,这将导致代码高度耦合并且难以测试,容器的引入正是为了解决上述的弊端。
struts2做为一个 web层的MVC实现框架,其核心功能主要是帮助我们处理 http请求,但是 struts2本身也包含了一个 IOC 容器,即XWork实现的框架级别的容器,用来支撑struts2的运行环境,并具备对象的获取和依赖注入功能,在搭建 ssh架构的时候,如果我们不配置 struts2和spring整合的插件,不把 Action转交给 spring-ioc来托管,那么struts2自身的ioc容器也足以满足我们的需求。而且Xwork的容器相较于spring更加小巧、精致。
二、IOC容器概述 ##
对于 一个ioc 容器来说,其核心功能是对象获取和依赖注入,在struts2 中容器的接口表示如下:
public interface Container extends Serializable {
/**
* Default dependency name.
*/
String DEFAULT_NAME = "default";
/**
* Injects dependencies into the fields and methods of an existing object.
* 进行对象依赖注入的基本操作接口,作为参数的object将被Xwork容器注入托管的对象(object内部声明有@Inject的字段和方法,都将被注入容器托管对象),从而完成依赖关系的建立
*/
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}.
* 获取容器中的java实例
*/
<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();
}
从Containter接口的表示方法中,我们可以非常直观的看出 ioc容器的对象获取和依赖注入这两个功能被表示为重载方法 getInstance 和 inject,这里我先对 getInstance(Class type, String name) 和 inject(Object ) 这个方法进行简单的分析。
Ioc容器的关系范围
从容器操作的接口角度:获取对象实例(getInstance)和实施依赖注入(inject),他们的操作对象有所不同。
获取对象
当调用容器的getInstance方法时,我们只能获取那些“被容器接管”的对象的实例。我们都知道struts2程序启动初始化时会去加载至少3个xml配置文件: struts-default.xml , struts-plugin.xml 和 struts.xml,其中前两者是struts2自带的框架级别的配置,后者是我们自己项目中的应用级别的配置,那么ioc容器所管理的对象就是在这其中配置的对象。分为三类:
- 在bean节点中声明的框架内部对象
- 在bean节点中声明的自定义对象
- 在constant节点和properties文件中声明的系统参数
下面是两个自定义的bean:
<bean type="com.wg.service.LoginServiceInter" name="loginservice" class="com.wg.service.impl.LoginServiceImpl" />
<bean type="com.wg.service.LoginServiceInter" name="loginservice1" class="com.wg.service.impl.LoginServiceImpl1" />
如果我想从 struts2-ioc 容器中获取 LoginServiceImpl对象,代码为:LoginServiceInter loginServiceInter =container.getInstance(LoginServiceInter.class,”loginservice”); //container为定义的容器
从这个方法中可以看出,以 type和name作为联合主键从ioc容器管理的对象中获取相应的对象!
对象的依赖注入
对于 inject(Object o),顾名思义,从字面上来看就知道是进行依赖注入操作,这里要强调的是,该方法是为参数Object o 这个对象注入在容器中管理的对象,建立起参数对象Object o 和容器管理对象之间的依赖关系, 这个参数对象 Object o 可以是容器管理的对象,也可以是我们自定义的对象,即它可以是任意对象,不必和getInstance(Class type, String name)方法那样只能是容器管理的对象。 那么容器怎么判断应该为该任意对象的哪些字段或者方法参数实施依赖注入呢?在 Spring中我们是通过 @Autowired 注解或者 在 xml 文件中配置依赖关系的。在 struts2的ioc实现中,则也是通过注解的方式实现,即 @Inject 来实现的,一旦我们在任意对象的方法参数、构造参数、字段上加入@Inject注解声明,那么就是在告诉ioc容器:请为我注入由容器管理的对象实例吧。
@Target({METHOD, CONSTRUCTOR, FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface Inject {
/**
* Dependency name. Defaults to {@link Container#DEFAULT_NAME}.
*/
String value() default DEFAULT_NAME;
/**
* Whether or not injection is required. Applicable only to methods and
* fields (not constructors or parameters).
*/
boolean required() default true;
}
以上 Inject 注解中的 value字段对应的就是之前我们在 xml 配置文件中的 name 属性,如之前的 “loginservice” 或 “loginservice1” ,其默认值为 “default”.Struts2通过此Inject注解,架起了任意对象与ioc容器进行通信的桥梁,使得受ioc容器管理的对象能够注入到任意对象对应的带Inject注解的方法参数和字段上。
操作实力
public class WaitLoginAction extends ActionSupport {
private String username;
private int progress;
private Container container;
private LoginServiceInter loginServiceInter;
//注入容器中name=loginservice1的托管对象
@Inject(value = "loginservice1")
private LoginServiceInter loginServiceInter1;
//注入全局的Container对象
@Inject
public void setContainer(Container container) {
this.container = container;
}
}
容器的初始化
提到IOC容器的初始化就不得不提到struts2 中另一种元素:package 的初始化,因为在struts2的初始化过程中不仅对容器初始化,也包含了对package这种事件映射元素的初始化,这里先简单说下这两者的区别,我们来看看常见的struts.xml中的配置:
<struts>
<constant name="struts.custom.i18n.resources" value="mess"></constant>
<constant name="struts.i18n.encoding" value="UTF-8"></constant>
<bean type="com.wg.service.LoginServiceInter" name="loginservice" class="com.wg.service.impl.LoginServiceImpl" />
<bean type="com.wg.service.LoginServiceInter" name="loginservice1" class="com.wg.service.impl.LoginServiceImpl1" />
<package name="org" extends="struts-default" >
<action name="login" class="org.wg.action.LoginAction" >
<result name="error">/error.jsp</result>
<result name="success">/welcome.jsp</result>
</action>>
</package>
</struts>
其中的 bean和constant节点,再加上 properties文件中的配置元素,这三者称为容器配置元素,即是由容器管理的对象,在容器初始化过程中要注册到容器中去。而对于 Package节点,里面包含了 action,interceptor,result等运行时的事件映射节点,这些节点元素并不需要纳入容器中管理。所以这里一个结论就是 struts2初始化的核心就是对容器配置元素和事件映射元素这两种不同元素的初始化过程,再进一步的讲就是将以各种形式配置的这两种元素转换为JAVA对象并统一管理的过程。由于这两种配置元素都有各种各样的表现形式,如之前看到的 xml配置的形式,属性文件properties的形式,还有我们进行扩展时自定义的其他形式,struts2插件中的 annotation形式等等。所以struts2在加载这些缤纷繁杂的各种配置形式时做了一定的考虑,先看如下的类图:
从上图中我们可以清楚的看到,针对两种不同的元素类型,struts2设计了两个接口: ContainterProvider和PackageProvider,它们对外提供的功能主要就是从配置文件中加载对应的元素并转换为JAVA对象。这里的 ConfigurationProvider 则是应用了java中的接口多重继承机制,目的在于对两种Provider加载器进一步抽象,使得我们不必关心具体的实现方式是针对哪种类型的加载器,提供一个统一的接口和操作方法;而且对于诸如struts.xml这样的配置文件来说,其中即包括了容器配置元素也包括了Package事件映射元素,那么对应的加载器必须同时具备ContainterProvider和PackageProvider的接口方法,这样就只需要实现 ConfigurationProvider 接口即可
Containter的创建初始化过程:
这里我们首先通过configuration的实现类创建一个ContainerBuilder,接着ContainerBuilder初始化我们最关心的Container,接着 在Configuration中循环调用了每一个Provider实现类的init()(初始化,即完成格式转换,如xml格式的转换为 document对象集合)和 register()方法,这个register()方法是在 ContainterProvider接口中声明的,所以该方法的作用就是往IOC容器(Container)中注册将要被容器托管的对象,可以想象下ContainerProvider 的register()方法肯定就是解析各种形式的容器配置元素,转化为Java对象,然后注册到容器的构造者对象 ContainerBuilder中去(事实上真正注册的还是对象工厂,而不是对象本身),其中的 factory()就是这个注册的作用,待所有的ContainterProvider实现类将各自对应的容器配置元素都注册到ContainerBuilder中之后,Configuration调用ContainerBuilder的create()方法就能返回一个被正确初始化了的IOC容器Container了。
public class DefaultConfiguration implements Configuration{
public synchronized List<PackageProvider> reloadContainer(List<ContainerProvider> providers) throws ConfigurationException {
packageContexts.clear();
loadedFileNames.clear();
List<PackageProvider> packageProviders = new ArrayList<PackageProvider>();
ContainerProperties props = new ContainerProperties();
ContainerBuilder builder = new ContainerBuilder();
Container bootstrap = createBootstrapContainer(providers);
for (final ContainerProvider containerProvider : providers)
{
bootstrap.inject(containerProvider);
//初始化格式,即完成格式转换
containerProvider.init(this);
//将资源文件中配置的对象注册到容器构造者对象中去。
containerProvider.register(builder, props);
}
props.setConstants(builder);
builder.factory(Configuration.class, new Factory<Configuration>() {
public Configuration create(Context context) throws Exception {
return DefaultConfiguration.this;
}
});
ActionContext oldContext = ActionContext.getContext();
try {
// Set the bootstrap container for the purposes of factory creation
setContext(bootstrap);
container = builder.create(false);
setContext(container);
objectFactory = container.getInstance(ObjectFactory.class);
// Process the configuration providers first
for (final ContainerProvider containerProvider : providers)
{
if (containerProvider instanceof PackageProvider) {
container.inject(containerProvider);
((PackageProvider)containerProvider).loadPackages();
packageProviders.add((PackageProvider)containerProvider);
}
}
// Then process any package providers from the plugins
Set<String> packageProviderNames = container.getInstanceNames(PackageProvider.class);
for (String name : packageProviderNames) {
PackageProvider provider = container.getInstance(PackageProvider.class, name);
provider.init(this);
provider.loadPackages();
packageProviders.add(provider);
}
rebuildRuntimeConfiguration();
} finally {
if (oldContext == null) {
ActionContext.setContext(null);
}
}
return packageProviders;
}
}
XmlConfigurationProvider为例子,来具体看一看它的register()方法。StrutsXmlConfigurationProvider主要是负责将 struts-default.xml,struts-plugin.xml和struts.xml中的容器配置元素和package配置元素加载到系统中来,它实现了 ConfigurationProvider接口,所以其具体的 register()方法就是解析xml文件中配置的容器配置元素并注册到ContainerBuilder中去,而具体的loadPackage()方法就是解析xml配置文件中的package元素并注册到package构造者对象PackageConfig.Builder中去(这是一个内部类).
public class XmlConfigurationProvider implements ConfigurationProvider{
public void register(ContainerBuilder containerBuilder, LocatableProperties props) throws ConfigurationException {
if (LOG.isInfoEnabled()) {
LOG.info("Parsing configuration file [" + configFileName + "]");
}
Map<String, Node> loadedBeans = new HashMap<String, Node>();
for (Document doc : documents) {
Element rootElement = doc.getDocumentElement();
NodeList children = rootElement.getChildNodes();
int childSize = children.getLength();
for (int i = 0; i < childSize; i++) {
Node childNode = children.item(i);
if (childNode instanceof Element) {
Element child = (Element) childNode;
final String nodeName = child.getNodeName();
if ("bean".equals(nodeName)) {
String type = child.getAttribute("type");
String name = child.getAttribute("name");
String impl = child.getAttribute("class");
String onlyStatic = child.getAttribute("static");
String scopeStr = child.getAttribute("scope");
boolean optional = "true".equals(child.getAttribute("optional"));
Scope scope = Scope.SINGLETON;
if ("default".equals(scopeStr)) {
scope = Scope.DEFAULT;
} else if ("request".equals(scopeStr)) {
scope = Scope.REQUEST;
} else if ("session".equals(scopeStr)) {
scope = Scope.SESSION;
} else if ("singleton".equals(scopeStr)) {
scope = Scope.SINGLETON;
} else if ("thread".equals(scopeStr)) {
scope = Scope.THREAD;
}
if (StringUtils.isEmpty(name)) {
name = Container.DEFAULT_NAME;
}
try {
Class cimpl = ClassLoaderUtil.loadClass(impl, getClass());
Class ctype = cimpl;
if (StringUtils.isNotEmpty(type)) {
ctype = ClassLoaderUtil.loadClass(type, getClass());
}
if ("true".equals(onlyStatic)) {
// Force loading of class to detect no class def found exceptions
cimpl.getDeclaredClasses();
containerBuilder.injectStatics(cimpl);
} else {
if (containerBuilder.contains(ctype, name)) {
Location loc = LocationUtils.getLocation(loadedBeans.get(ctype.getName() + name));
if (throwExceptionOnDuplicateBeans) {
throw new ConfigurationException("Bean type " + ctype + " with the name " +
name + " has already been loaded by " + loc, child);
}
}
// Force loading of class to detect no class def found exceptions
cimpl.getDeclaredConstructors();
if (LOG.isDebugEnabled()) {
LOG.debug("Loaded type:" + type + " name:" + name + " impl:" + impl);
}
// LocatableFactory 的create方法中真正调用 containterImpl中的 inject(Class clas)方法进行对象的创建和注入; containerBuilder.factory(ctype, name, new LocatableFactory(name, ctype, cimpl, scope, childNode), scope);
}
loadedBeans.put(ctype.getName() + name, child);
} catch (Throwable ex) {
if (!optional) {
throw new ConfigurationException("Unable to load bean: type:" + type + " class:" + impl, ex, childNode);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Unable to load optional class: #0", impl);
}
}
}
} else if ("constant".equals(nodeName)) {
String name = child.getAttribute("name");
String value = child.getAttribute("value");
props.setProperty(name, value, childNode);
} else if (nodeName.equals("unknown-handler-stack")) {
List<UnknownHandlerConfig> unknownHandlerStack = new ArrayList<UnknownHandlerConfig>();
NodeList unknownHandlers = child.getElementsByTagName("unknown-handler-ref");
int unknownHandlersSize = unknownHandlers.getLength();
for (int k = 0; k < unknownHandlersSize; k++) {
Element unknownHandler = (Element) unknownHandlers.item(k);
Location location = LocationUtils.getLocation(unknownHandler);
unknownHandlerStack.add(new UnknownHandlerConfig(unknownHandler.getAttribute("name"), location));
}
if (!unknownHandlerStack.isEmpty())
configuration.setUnknownHandlerStack(unknownHandlerStack);
}
}
}
}
}
}
注意这行代码:
containerBuilder.factory(ctype, name, new LocatableFactory(name, ctype, cimpl, scope, childNode), scope);
这行代码的作用就是往容器构造对象ContainerBuilder中注册将要被容器接纳托管的对象,我们之前说过,在调用 Container.getInstance 方法的时候,IOC容器是以参数 type和name作为联合主键从容器中获取对象的,那么这里在初始化时往容器中注册对象的时候 ctype和name就是这个联合主键了,其对应的值一般认为就是对应的对象了,但是这里貌似不对,应该我们看到的值是 LocatableFactory,这好像是一个对象工厂,而不是对象本身吧
public class LocatableFactory<T> extends Located implements Factory<T> {
private Class implementation;
private Class type;
private String name;
private Scope scope;
public LocatableFactory(String name, Class type, Class implementation, Scope scope, Object location) {
this.implementation = implementation;
this.type = type;
this.name = name;
this.scope = scope;
setLocation(LocationUtils.getLocation(location));
}
@SuppressWarnings("unchecked")
public T create(Context context) {
Object obj = context.getContainer().inject(implementation);
return (T) obj;
}
/*这里省略N多代码*/
这里面看到了 create 方法,它根据我们传入的 Class 对象返回了一个Object实例,再来看看 Factory接口:
/**
* A custom factory. Creates objects which will be injected.
*
* @author crazybob@google.com (Bob Lee)
*/
public interface Factory<T> {
/**
* Creates an object to be injected.
*
* @param context of this injection
* @return instance to be injected
* @throws Exception if unable to create object
*/
T create(Context context) throws Exception;
}
到这里,我们仿佛明白了,容器中接纳和托管的原来不是对象本身,而是对象工厂,当我们需要容器提供一个对象的时候,容器是调用的对象工厂中的 create 方法来创建并返回对象的。而对于具体的对象创建方式,我们可以通过实现Factory接口,实现其create方法即可,这里的Factory实现类为LocatableFactory,其create方法为调用Container的重载方法 inject(Class cl) 创建并返回一个新对象,该对象已经接受过容器的依赖注入了。具体的 inject(Class cl)实现细节,我们留到后面介绍容器操作方法 inject 和 getInstance 的时候再详细说明。当然了,如果我们自定义了别的Factory实现类,我们在 create 方法中完全可以根据我们的需要实现任意的对象创建方法,比如: class.newInstance() 这种最基本的方式或者从 JSON串转换为一个JAVA对象,或者反序列化创建对象等等,这就是工厂方法模式的优点,创建对象的方式可以很灵活。
public final class ContainerBuilder {
final Map<Key<?>, InternalFactory<?>> factories =
new HashMap<Key<?>, InternalFactory<?>>();
final List<InternalFactory<?>> singletonFactories =
new ArrayList<InternalFactory<?>>();
boolean created;
/*省略N多代码*/
}
容器内部缓存的确实是对象工厂(factories )
class ContainerImpl implements Container {
final Map<Key<?>, InternalFactory<?>> factories;
final Map<Class<?>,Set<String>> factoryNamesByType;
ContainerImpl(Map<Key<?>, InternalFactory<?>> factories) {
this.factories = factories;
Map<Class<?>,Set<String>> map = new HashMap<Class<?>,Set<String>>();
for (Key<?> key : factories.keySet()) {
Set<String> names = map.get(key.getType());
if (names == null) {
names = new HashSet<String>();
map.put(key.getType(), names);
}
names.add(key.getName());
}
for (Entry<Class<?>,Set<String>> entry : map.entrySet()) {
entry.setValue(Collections.unmodifiableSet(entry.getValue()));
}
this.factoryNamesByType = Collections.unmodifiableMap(map);
}
/*此处省略N多代码*/
}
这里是Container接口的具体实现类 ContainerImpl.
ContainerImpl container = new ContainerImpl(
new HashMap<Key<?>, InternalFactory<?>>(factories));
//factories为ContainerBuilder 建造的
到此为止,关于Struts2-IOC容器的初始化过程的重点部分已经讲述完毕了,我们发现其重点就是对象工厂的层层包装,即装饰模式的运用,这样当我们要容器要一个对象实例的时候,就会触发一系列的 InternalFactory.create 方法调用。以及容器对象的建造采用了建造者模式。核心结论是容器初始化完毕后其内部的Map字段factories中缓存的是 对象工厂,而不是对象实例本身。