Spring源码学习(2)——默认标签的解析

上一篇随笔说到Spring对于默认标签和自定义标签的解析方法是不同的,这里详细看一下Spring对于默认标签的解析。

    private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if(delegate.nodeNameEquals(ele, "import")) {
this.importBeanDefinitionResource(ele);
} else if(delegate.nodeNameEquals(ele, "alias")) {
this.processAliasRegistration(ele);
} else if(delegate.nodeNameEquals(ele, "bean")) {
this.processBeanDefinition(ele, delegate);
} else if(delegate.nodeNameEquals(ele, "beans")) {
this.doRegisterBeanDefinitions(ele);
} }

在这里对根节点的子节点的四种不同类型分别做了不同的处理。这四种解析中,对bean的解析最为复杂。所以我们进入函数processBeanDefinition。

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if(bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); try {
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.getReaderContext().getRegistry());
} catch (BeanDefinitionStoreException var5) {
this.getReaderContext().error("Failed to register bean definition with name \'" + bdHolder.getBeanName() + "\'", ele, var5);
} this.getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
} }

这段代码中:

1、首先委托了BeanDefinitionParseDelegate对节点做了解析,并返回了一个BeanDefinitionHolder的实例,在这个实例中已经包含了配置文件中配置的各种属性了

2、如果在当前子节点中存在自定义属性,则还需要对自定义标签进行解析

3、解析完成后,需要对解析后的bdHolder进行注册,同样注册操作委托给了BeanDefinitionReaderUtils

4、最后发出响应事件,通知相关的监听器

我们先看解析节点的部分

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
String id = ele.getAttribute("id");
String nameAttr = ele.getAttribute("name");
ArrayList aliases = new ArrayList();
if(StringUtils.hasLength(nameAttr)) {
String[] beanName = StringUtils.tokenizeToStringArray(nameAttr, ",; ");
aliases.addAll(Arrays.asList(beanName));
} String beanName1 = id;
if(!StringUtils.hasText(id) && !aliases.isEmpty()) {
beanName1 = (String)aliases.remove(0);
if(this.logger.isDebugEnabled()) {
this.logger.debug("No XML \'id\' specified - using \'" + beanName1 + "\' as bean name and " + aliases + " as aliases");
}
} if(containingBean == null) {
this.checkNameUniqueness(beanName1, aliases, ele);
} AbstractBeanDefinition beanDefinition = this.parseBeanDefinitionElement(ele, beanName1, containingBean);
if(beanDefinition != null) {
if(!StringUtils.hasText(beanName1)) {
try {
if(containingBean != null) {
beanName1 = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true);
} else {
beanName1 = this.readerContext.generateBeanName(beanDefinition);
String aliasesArray = beanDefinition.getBeanClassName();
if(aliasesArray != null && beanName1.startsWith(aliasesArray) && beanName1.length() > aliasesArray.length() && !this.readerContext.getRegistry().isBeanNameInUse(aliasesArray)) {
aliases.add(aliasesArray);
}
} if(this.logger.isDebugEnabled()) {
this.logger.debug("Neither XML \'id\' nor \'name\' specified - using generated bean name [" + beanName1 + "]");
}
} catch (Exception var9) {
this.error(var9.getMessage(), ele);
return null;
}
} String[] aliasesArray1 = StringUtils.toStringArray(aliases);
return new BeanDefinitionHolder(beanDefinition, beanName1, aliasesArray1);
} else {
return null;
}
}

这段代码中首先提取了当前节点的id和name,beanName默认为id,如果id为空,则spring会去别名中的第一个作为beanName。

然后spring会检查当前beanName是否唯一,在BeanDefinitionParseDelegate中维护了一套已用过的beanName以及alias的集合,如果bean的beanName和alias和已存在的名字重复,则会抛错。

进一步解析节点的所有属性,并统一封装到GenericBeanDefinition中。

如果发现当前bean的beanName为空,则使用默认规则为其自动生成beanName。

最后将所有信息封装到BeanDefinitionHolder中返回。

进入到parseBeanDefinitionElement方法中

public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, BeanDefinition containingBean) {
this.parseState.push(new BeanEntry(beanName));
String className = null;
if(ele.hasAttribute("class")) {
className = ele.getAttribute("class").trim();
} try {
String ex = null;
if(ele.hasAttribute("parent")) {
ex = ele.getAttribute("parent");
} AbstractBeanDefinition bd = this.createBeanDefinition(className, ex);
this.parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, "description"));
this.parseMetaElements(ele, bd);
this.parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
this.parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
this.parseConstructorArgElements(ele, bd);
this.parsePropertyElements(ele, bd);
this.parseQualifierElements(ele, bd);
bd.setResource(this.readerContext.getResource());
bd.setSource(this.extractSource(ele));
AbstractBeanDefinition var7 = bd;
return var7;
} catch (ClassNotFoundException var13) {
this.error("Bean class [" + className + "] not found", ele, var13);
} catch (NoClassDefFoundError var14) {
this.error("Class that bean class [" + className + "] depends on not found", ele, var14);
} catch (Throwable var15) {
this.error("Unexpected failure during bean definition parsing", ele, var15);
} finally {
this.parseState.pop();
} return null;
}

在this.createBeanDefinition方法中,Spring会生产一个GenericBeanDefinition实例,在其属性里设置了当前节点的父类parent,以及className类名的信息。

在this.parseBeanDefinitionAttributes方法中对于一些属性进行了解析

public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, BeanDefinition containingBean, AbstractBeanDefinition bd) {
if(ele.hasAttribute("singleton")) {
this.error("Old 1.x \'singleton\' attribute in use - upgrade to \'scope\' declaration", ele);
} else if(ele.hasAttribute("scope")) {
bd.setScope(ele.getAttribute("scope"));
} else if(containingBean != null) {
bd.setScope(containingBean.getScope());
} if(ele.hasAttribute("abstract")) {
bd.setAbstract("true".equals(ele.getAttribute("abstract")));
} String lazyInit = ele.getAttribute("lazy-init");
if("default".equals(lazyInit)) {
lazyInit = this.defaults.getLazyInit();
} bd.setLazyInit("true".equals(lazyInit));
String autowire = ele.getAttribute("autowire");
bd.setAutowireMode(this.getAutowireMode(autowire));
String dependencyCheck = ele.getAttribute("dependency-check");
bd.setDependencyCheck(this.getDependencyCheck(dependencyCheck));
String autowireCandidate;
if(ele.hasAttribute("depends-on")) {
autowireCandidate = ele.getAttribute("depends-on");
bd.setDependsOn(StringUtils.tokenizeToStringArray(autowireCandidate, ",; "));
} autowireCandidate = ele.getAttribute("autowire-candidate");
String destroyMethodName;
if(!"".equals(autowireCandidate) && !"default".equals(autowireCandidate)) {
bd.setAutowireCandidate("true".equals(autowireCandidate));
} else {
destroyMethodName = this.defaults.getAutowireCandidates();
if(destroyMethodName != null) {
String[] patterns = StringUtils.commaDelimitedListToStringArray(destroyMethodName);
bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName));
}
} if(ele.hasAttribute("primary")) {
bd.setPrimary("true".equals(ele.getAttribute("primary")));
} if(ele.hasAttribute("init-method")) {
destroyMethodName = ele.getAttribute("init-method");
if(!"".equals(destroyMethodName)) {
bd.setInitMethodName(destroyMethodName);
}
} else if(this.defaults.getInitMethod() != null) {
bd.setInitMethodName(this.defaults.getInitMethod());
bd.setEnforceInitMethod(false);
} if(ele.hasAttribute("destroy-method")) {
destroyMethodName = ele.getAttribute("destroy-method");
if(!"".equals(destroyMethodName)) {
bd.setDestroyMethodName(destroyMethodName);
}
} else if(this.defaults.getDestroyMethod() != null) {
bd.setDestroyMethodName(this.defaults.getDestroyMethod());
bd.setEnforceDestroyMethod(false);
} if(ele.hasAttribute("factory-method")) {
bd.setFactoryMethodName(ele.getAttribute("factory-method"));
} if(ele.hasAttribute("factory-bean")) {
bd.setFactoryBeanName(ele.getAttribute("factory-bean"));
} return bd;
}

在这里我们可以看到其中不少属性是我们经常使用的,当然也有一些属性不太熟悉。这里大致查了一下资料,简单介绍这些属性的意义:

scope:

对象在spring容器(IOC容器)中的生命周期。共有五中取值。singleton和protoType大家比较熟悉。另外三种request,session和global session只适用于web应用。

abstract:

当前bean是否为抽象类,默认为false

lazy-init:

延迟初始化,在ApplicationContext中默认配置时,spring会在应用启动时就对容器中的bean进行实例化,如果设置了延迟加载,则spring会在第一次使用该bean时初始化该bean。

autowire:

设置bean自动装配模式。可选5种模式   no:不使用自动装配。Bean的引用必须通过ref元素定义。byName:通过名字进行装配  byType:根据类型进行装配 constructor:和byType类似,不过是应用于通过构造函数注入的方式。autoDetect:通过对bean检查类的内部自动选择constructor还是byType。

dependency-check:

依赖检查的模式。

depends-on:

实例化时,依赖的bean,spring会在实例化当前bean之前先实例化依赖的bean。一般不需特别设置,spring会提供一套默认对依赖的检查和实例化的逻辑。

autowire-candidate:

当自动注入的类的实现类有多个时,有两种方案,一种是设置某一个实现类的autowire-candidate属性为false,则该类不参与自动注入。或者设置依赖注入的对象的autowire-candidate为指定实现类。

primary:

primary属性为true时,自动装配时当出现多个Bean的候选者时,属性primary=true的Bean被作为首选者。

init-method:

定义spring 容器在初始化bean之前的所做的操作。

destroy-method:

定义spring 容器在容器销毁之前的所做的操作。

factory-method 和 factory-bean:

spring IoC注入方式除了我们常见的set注入和构造器注入,还可以通过工厂方式注入。工厂分静态工厂和普通工厂。

如果想用某一个静态工厂来实例化bean,可以使用如下配置:

<bean name="production" class="com.wuzhe.factory.StaticFactory" factory-method="getProduction"/>

如果想通过普通工厂来实例化bean,可以使用如下配置:

<bean name="factory" class="com.wuzhe.factory.Factory"/>
<bean name="production" factory-bean="factory" factory-method="getProduction"/>

接下来是解析子节点的meta属性:  this.parseMetaElements(ele, bd)

meta属性是在定义bean时额外添加的说明。例如:

    <bean id="myBean" class="com.wuzhe.bean.MyBean">
<meta key="author" value="wuzhe"></meta>
</bean>

这里的meta属性并不会体现在myBean中,当需要meta信息的时候可以通过BeanDefinition.getAttribute(key)方法获取

接下来是解析节点的lookup-method:

this.parseLookupOverrideSubElements(ele, bd.getMethodOverrides());

先来看一下这个属性的用法,我们把lookup-method称之为获取器注入。这是一种特殊的方法注入,它是把一个方法声明为返回某个类型的bean,这样可用在设计有些可插拔的功能上,解除程序依赖。

举例说明,假设GetFood类中有一个Food getFood()方法。然后Bread和Cake分别继承Food类。当我们希望getFood()返回Bread类型的bean,我们可以这样配置:

    <bean id="getFoodTest" class="com.wuzhe.lookup.GetFood">
<lookup-method name="getFood" bean="bread"/>
</bean>
<bean id="bread" class="com.wuzhe.food.Bread"/>
<bean id="cake" class="com.wuzhe.food.Cake"/>

如果我们想让getFood()方法返回cake时,只要修改bean属性就可以了

进入到解析lookup-method方法里面

    public void parseLookupOverrideSubElements(Element beanEle, MethodOverrides overrides) {
NodeList nl = beanEle.getChildNodes(); for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if(this.isCandidateElement(node) && this.nodeNameEquals(node, "lookup-method")) {
Element ele = (Element)node;
String methodName = ele.getAttribute("name");
String beanRef = ele.getAttribute("bean");
LookupOverride override = new LookupOverride(methodName, beanRef);
override.setSource(this.extractSource(ele));
overrides.addOverride(override);
}
} }

上面这段代码会将lookup-method要mock返回的方法和bean封装在LookupOverride类中,并添加到bd的methodOverrides属性中。

接下来是解析replace-method属性

this.parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

replace-method更加强大,不但可以替换bean,还可以替换原有方法的逻辑。它可以将bean中的方法替换成另一个继承MethodReplace的类reimplement的实现

public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) {
NodeList nl = beanEle.getChildNodes(); for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if(this.isCandidateElement(node) && this.nodeNameEquals(node, "replaced-method")) {
Element replacedMethodEle = (Element)node;
String name = replacedMethodEle.getAttribute("name");
String callback = replacedMethodEle.getAttribute("replacer");
ReplaceOverride replaceOverride = new ReplaceOverride(name, callback);
List argTypeEles = DomUtils.getChildElementsByTagName(replacedMethodEle, "arg-type");
Iterator var11 = argTypeEles.iterator(); while(var11.hasNext()) {
Element argTypeEle = (Element)var11.next();
String match = argTypeEle.getAttribute("match");
match = StringUtils.hasText(match)?match:DomUtils.getTextValue(argTypeEle);
if(StringUtils.hasText(match)) {
replaceOverride.addTypeIdentifier(match);
}
} replaceOverride.setSource(this.extractSource(replacedMethodEle));
overrides.addOverride(replaceOverride);
}
} }

这里解析replace-method的思路和lookup-method类似,都是讲元素的属性封装一下,并添加到bd的methodOverrides属性中。不同的是replace-method属性封装在RepalceOverride中。

接下来看对构造函数的解析

 public void parseConstructorArgElement(Element ele, BeanDefinition bd) {
String indexAttr = ele.getAttribute("index");
String typeAttr = ele.getAttribute("type");
String nameAttr = ele.getAttribute("name");
if(StringUtils.hasLength(indexAttr)) {
try {
int value = Integer.parseInt(indexAttr);
if(value < 0) {
this.error("\'index\' cannot be lower than 0", ele);
} else {
try {
this.parseState.push(new ConstructorArgumentEntry(value));
Object valueHolder = this.parsePropertyValue(ele, bd, (String)null);
ValueHolder valueHolder1 = new ValueHolder(valueHolder);
if(StringUtils.hasLength(typeAttr)) {
valueHolder1.setType(typeAttr);
} if(StringUtils.hasLength(nameAttr)) {
valueHolder1.setName(nameAttr);
} valueHolder1.setSource(this.extractSource(ele));
if(bd.getConstructorArgumentValues().hasIndexedArgumentValue(value)) {
this.error("Ambiguous constructor-arg entries for index " + value, ele);
} else {
bd.getConstructorArgumentValues().addIndexedArgumentValue(value, valueHolder1);
}
} finally {
this.parseState.pop();
}
}
} catch (NumberFormatException var19) {
this.error("Attribute \'index\' of tag \'constructor-arg\' must be an integer", ele);
}
} else {
try {
this.parseState.push(new ConstructorArgumentEntry());
Object value1 = this.parsePropertyValue(ele, bd, (String)null);
ValueHolder valueHolder2 = new ValueHolder(value1);
if(StringUtils.hasLength(typeAttr)) {
valueHolder2.setType(typeAttr);
} if(StringUtils.hasLength(nameAttr)) {
valueHolder2.setName(nameAttr);
} valueHolder2.setSource(this.extractSource(ele));
bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder2);
} finally {
this.parseState.pop();
}
} }

spring对constructor-arg是否存在index属性的处理流程稍有不同,不同在于如果存在index属性,spring会将type、name和index属性封装在ValueHolder并添加至bd的constructorArgumentValues的indexedArgumentValues属性中,如果不存在则添加至genericArgumentValue属性中。

解析property属性和constructor-arg的过程类似

public void parsePropertyElement(Element ele, BeanDefinition bd) {
String propertyName = ele.getAttribute("name");
if(!StringUtils.hasLength(propertyName)) {
this.error("Tag \'property\' must have a \'name\' attribute", ele);
} else {
this.parseState.push(new PropertyEntry(propertyName)); try {
if(bd.getPropertyValues().contains(propertyName)) {
this.error("Multiple \'property\' definitions for property \'" + propertyName + "\'", ele);
return;
} Object val = this.parsePropertyValue(ele, bd, propertyName);
PropertyValue pv = new PropertyValue(propertyName, val);
this.parseMetaElements(ele, pv);
pv.setSource(this.extractSource(ele));
bd.getPropertyValues().addPropertyValue(pv);
} finally {
this.parseState.pop();
} }
}

解析qualifier

Spring在进行自动注入时,容器中候选的Bean有且只有一个,当有多个候选Bean的时候可以通过Qualifier指定注入Bean的名称。

具体解析过程和之前大同小异

public void parseQualifierElement(Element ele, AbstractBeanDefinition bd) {
String typeName = ele.getAttribute("type");
if(!StringUtils.hasLength(typeName)) {
this.error("Tag \'qualifier\' must have a \'type\' attribute", ele);
} else {
this.parseState.push(new QualifierEntry(typeName)); try {
AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(typeName);
qualifier.setSource(this.extractSource(ele));
String value = ele.getAttribute("value");
if(StringUtils.hasLength(value)) {
qualifier.setAttribute(AutowireCandidateQualifier.VALUE_KEY, value);
} NodeList nl = ele.getChildNodes(); for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if(this.isCandidateElement(node) && this.nodeNameEquals(node, "attribute")) {
Element attributeEle = (Element)node;
String attributeName = attributeEle.getAttribute("key");
String attributeValue = attributeEle.getAttribute("value");
if(!StringUtils.hasLength(attributeName) || !StringUtils.hasLength(attributeValue)) {
this.error("Qualifier \'attribute\' tag must have a \'name\' and \'value\'", attributeEle);
return;
} BeanMetadataAttribute attribute = new BeanMetadataAttribute(attributeName, attributeValue);
attribute.setSource(this.extractSource(attributeEle));
qualifier.addMetadataAttribute(attribute);
}
} bd.addQualifier(qualifier);
} finally {
this.parseState.pop();
}
}
}

至此,对bean节点的属性解析就完成了,相当于spring将配置文件转换成了内存中的GenericBeanDefinition对象。xml中的属性都可以在GenericBeanDefinition找到储存的位置。

解析完bean后,就需要对beanDefinition进行注册,注册我理解就是把解析的beanDefinition存放在内存中,统一管理。在XmlFactory中,所有的beanDefinition缓存都存放在了它的父类DefaultListableBeanFactory的

beanDefinitionMap中,key值为beanName。

我们看一下注册的源码

 public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
if(beanDefinition instanceof AbstractBeanDefinition) {
try {
((AbstractBeanDefinition)beanDefinition).validate();
} catch (BeanDefinitionValidationException var9) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", var9);
}
} BeanDefinition oldBeanDefinition = (BeanDefinition)this.beanDefinitionMap.get(beanName);
if(oldBeanDefinition != null) {
if(!this.isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition [" + beanDefinition + "] for bean \'" + beanName + "\': There is already [" + oldBeanDefinition + "] bound.");
} if(oldBeanDefinition.getRole() < beanDefinition.getRole()) {
if(this.logger.isWarnEnabled()) {
this.logger.warn("Overriding user-defined bean definition for bean \'" + beanName + "\' with a framework-generated bean definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
}
} else if(!beanDefinition.equals(oldBeanDefinition)) {
if(this.logger.isInfoEnabled()) {
this.logger.info("Overriding bean definition for bean \'" + beanName + "\' with a different definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
}
} else if(this.logger.isDebugEnabled()) {
this.logger.debug("Overriding bean definition for bean \'" + beanName + "\' with an equivalent definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
} this.beanDefinitionMap.put(beanName, beanDefinition);
} else {
if(this.hasBeanCreationStarted()) {
Map var4 = this.beanDefinitionMap;
synchronized(this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
ArrayList updatedDefinitions = new ArrayList(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
if(this.manualSingletonNames.contains(beanName)) {
LinkedHashSet updatedSingletons = new LinkedHashSet(this.manualSingletonNames);
updatedSingletons.remove(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
} else {
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
this.manualSingletonNames.remove(beanName);
} this.frozenBeanDefinitionNames = null;
} if(oldBeanDefinition != null || this.containsSingleton(beanName)) {
this.resetBeanDefinition(beanName);
} }

在这段代码中

1、首先对传入的beanDefinition做了合法性的校验

2、然后根据beanName查找当前beanDefinitionMap缓存中是否已有相应的beanDefinion。如果有且当前设置了不允许bean覆盖,则会抛错,若允许覆盖,则用当前beanDefinition覆盖旧的beanDefinition,并清除缓存中旧的bean信息。

3、如果beanDefinitionMap缓存中不存在旧的beanDifinition, 则在beanDefinitionMap中存放当前beanDefinition。

这里涉及到的几个缓存:beanDefinitionMap很好理解,manualSingletonNames的含义并不清楚,这里留个疑问,后续若学习到相关知识,再来补充。

注册好了beanDefinition,接下来就是注册alias。注册的alias和beanName的对应关系存放在了aliasMap中

    public void registerAlias(String name, String alias) {
Assert.hasText(name, "\'name\' must not be empty");
Assert.hasText(alias, "\'alias\' must not be empty");
if(alias.equals(name)) {
this.aliasMap.remove(alias);
} else {
String registeredName = (String)this.aliasMap.get(alias);
if(registeredName != null) {
if(registeredName.equals(name)) {
return;
} if(!this.allowAliasOverriding()) {
throw new IllegalStateException("Cannot register alias \'" + alias + "\' for name \'" + name + "\': It is already registered for name \'" + registeredName + "\'.");
}
} this.checkForAliasCircle(name, alias);
this.aliasMap.put(alias, name);
} }

注册别名的过程还是很容易理解的:

1、若beanName和alias相同,则无需处理,并在缓存中删除alias

2、若存在alias注册了其他beanName,则根据是否允许覆盖的设置,进行相应处理

3、循环检查alias

4、注册alias

至此对bean默认标签的解析和注册就完成了

上一篇:【spring源码分析】IOC容器初始化(十二)


下一篇:struts.xml框架