一、容器的引入
在面向對象的世界裏,業務邏輯的實現通常是由多個對象相互協作完成的,這使得每個對象都需要,與其合作的對象(也就是它所依賴的對象)的引用。如果這個獲取過程要靠自身實現,那麼如你所見,這將導致代碼高度耦合並且難以測試,容器的引入正是爲了解決上述的弊端。
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中緩存的是 對象工廠,而不是對象實例本身。