近期希望能够对Spring有一个系统性的学习,由于现在最新版本的Spring代码量已经比较庞大,希望通过使用早期版本Spring作为蓝本来学习和使用的方法,以便快速上手,另外可以更容易的对源码进行分析和解读。
总体上打算从Spring的内核Beans框架开始,学习使用第一个范例。
环境搭建
- 从Oracle站点上下载j2sdk 1.3.1, 以及j2ee 1.3, 并安装到本地 (很复古的节奏
,只是为了配合Spring v0.9的版本来使用)
- 从SourceForge网站上下载Spring v0.9 release
- 在Eclipse里面新建Java工程,添加build依赖项,Spring v0.9 的所有lib, Java 1.3 JRE Lib, 添加j2ee 1.3 lib目录下的j2ee.jar
代码实现
代码很简单,通过beans XML配置来加载和使用Role这个类
Role.java
package test.beans;
public class Role {
private String name;
public String getName() {
return name;
}
public String setName() {
this.name = name;
}
}
Main.java
package test.beans;
import com.interface21.beans.factory.xml.XmlBeanFactory;
public class Main {
public static void main(String[] args) {
try {
XmlBeanFactory factory = new XmlBeanFactory("bin/my_bean.xml");
Role t = (Role) factory.getBean("teacher");
System.out.println(t.getName());
} catch (Throwable e) {
}
}
}
my_beans.xml
<?xml version="1.0"?>
<beans>
<bean id="teacher" class="test.beans.Role">
<property name="name">
<value>Teacher</value>
</property>
</bean>
</beans>
运行Main.java,结果出现下面的错误信息,
java.lang.NoSuchMethodError
at com.interface21.beans.factory.xml.XmlBeanFactory$BeansErrorHandler.warning(XmlBeanFactory.java:460)
at org.apache.crimson.parser.Parser2.warning(Parser2.java:3153)
at org.apache.crimson.parser.Parser2.parseInternal(Parser2.java:491)
at org.apache.crimson.parser.Parser2.parse(Parser2.java:305)
at org.apache.crimson.parser.XMLReaderImpl.parse(XMLReaderImpl.java:442)
at org.apache.crimson.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:185)
at javax.xml.parsers.DocumentBuilder.parse(DocumentBuilder.java:122)
at com.interface21.beans.factory.xml.XmlBeanFactory.loadBeanDefinitions(XmlBeanFactory.java:199)
at com.interface21.beans.factory.xml.XmlBeanFactory.<init>(XmlBeanFactory.java:124)
at com.interface21.beans.factory.xml.XmlBeanFactory.<init>(XmlBeanFactory.java:136)
at test.beans.Main.main(Main.java:10)
原本以为很顺理成章就能运行成功第一个例子,然而还是遇到问题,先试着定位一下,查看XmlBeanFactory.java代码,代码中有以下的片段,
private void loadBeanDefinition(InputStream is) throws BeansException {
....
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(true);
DocumentBuilder db = factory.newDocumentBuilder();
db.setErrorHandler(new BeansErrorHandler());
db.setEntityResolver(new BeansDtdResolver());
Document doc = db.parse(is);
loadBeanDefinitions(doc);
} catch (ParserConfigurationException ex) {
}
...
}
这里实现设置了错误处理回调BeansErrorHandler,于是在该类中设置断点来查看错误,以debug方式启动,在BeansErrorHandler中触发断点,
查看错误,显示"valid documents must have a <!DOCTYPE declaration"
因此问题是出在my_beans.xml的定义文件格式上,增加上面所提到的内容如下,
<?xml version="1.0"?>
<!DOCTYPE beans SYSTEM "spring-beans.dtd">
<beans>
<bean id="teacher" class="test.beans.Role">
<property name="name">
<value>Teacher</value>
</property>
</bean>
</beans>
再次运行,结果出现了另外的异常,Relative URI "spring-beans.dtd"; can not be resolved without a base URI.
com.interface21.beans.factory.BeanDefinitionStoreException: XML document is invalid; nested exception is:
org.xml.sax.SAXParseException: Relative URI "spring-beans.dtd"; can not be resolved without a base URI.
at com.interface21.beans.factory.xml.XmlBeanFactory.loadBeanDefinitions(XmlBeanFactory.java:206)
at com.interface21.beans.factory.xml.XmlBeanFactory.<init>(XmlBeanFactory.java:124)
at com.interface21.beans.factory.xml.XmlBeanFactory.<init>(XmlBeanFactory.java:136)
at test.beans.Main.main(Main.java:10)
在网上搜索类似的异常及解决方案,找到了几个相关联的链接在讨论类似的问题,提到在DOCTYPE标签中使用了spring官方站点的DTD文件链接,提出问题如何改造为本地DTD位置,这样可以避免不必要的联网下载。
另外,还有一个链接给出了两个解决方案,http://www.jdom.org/pipermail/jdom-interest/2002-November/011086.html
首先,确保DTD文件在相对于XML文件的相对路径上能够找到,设置SAX InputSource的System ID以使其解析器能够定位到XML文件,以此路径为base URI来定位DTD文件的位置。
或者,更普遍的方案,实现一个org.xml.sax.EntityResolver并将其注册到SAXBuilder,通过这样的方式,定位DTD文件位置这件事情就delegate给应用程序自身,所以DTD文件可以放置在任意方便的位置,例如打包在程序的Jar文件中。
其实,仔细阅读前面的代码,就会发现Spring就是采用的第二种方式,
db.setEntityResolver(new BeansDtdResolver());
这里BeansDtdResolver就是负责定位DTD文件,并将文件stream返回给XML解析器。代码如下:
public class BeansDtdResolver implements EntityResolver {
private static final String DTD_NAME = "spring-beans";
private static final String SEARCH_PACKAGE = "/com/interface21/beans/factory/xml/";
private Log log = LogFactory.getLog(getClass());
public InputSource resolveEntity(String publicId, String systemId) {
if (systemId != null && systemId.indexOf(DTD_NAME) > systemId.lastIndexOf("/")) {
String dtdFile = systemId.substring(systemId.indexOf(DTD_NAME));
// Search for DTD
log.debug("Trying to locate [" + dtdFile + "] under " + SEARCH_PACKAGE);
InputStream in = getClass().getResourceAsStream(SEARCH_PACKAGE + dtdFile);
if (in != null) {
log.debug("Found DTD [" + systemId + "] in classpath");
InputSource source = new InputSource(in);
source.setPublicId(publicId);
source.setSystemId(systemId);
return source;
}
else {
log.debug("Could not resolve DTD [" + systemId + "]: not found in classpath");
}
}
else {
log.debug("Ignoring DTD [" + systemId + "]");
}
// use the default behaviour -> download from website
return null;
}
}
但是,尽管如此,在这个方法中设置断点,却发现程序没有跑进去,运行结果依然出现“can not be resolved without a base URI.”的问题,后来发现需要使用spring-beans.dtd的网络URL地址才能正常工作,更新my_beans.xml文件如下,
<?xml version="1.0"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="teacher" class="test.beans.Role">
<property name="name">
<value>Teacher</value>
</property>
</bean>
</beans>
这个时候再次调试,程序就可以跑进上述的resolveEntity()方法中,通过调试,发现会在systemId参数中传入DOCTYPE标签最后一个属性值,http://www.springframework.org/dtd/spring-beans.dtd,这样在resolveEntity()方法中,它先提取出文件名sping-beans.dtd,然后和jar中打包的该文件的相对路径做拼接,再直接通过资源加载的方式去加载该dtd文件。
再试着运行程序,结果能够正常运行。
在尝试完上述基于JDK1.3的环境之后,我又试着在JDK8下面运行同样的工程(在JDK8下面,只需要加入Spring 0.9 release jar包以及JDK8 runtime lib就可以),发现直接采用下面本地路径的方式也能正常工作。
<!DOCTYPE beans SYSTEM "spring-beans.dtd">
对比解析XML的调用栈发现,JDK1.3下的XML解析和JDK8下的XML解析的调用栈有比较大的差异,也许原因就在于对XML的解析流程的差异造成了最终的运行时表现的不同。
JDK1.3下的XML解析调用栈
JDK8下的XML解析调用栈
小结
本文记录了在基于Spring v0.9版本运行的第一个程序过程中遇到的问题及对应的解决方法,主要是先做一个记录,也同时分享给遇到类似错误的同学,希望这些小的发现能够对解决类似问题提供些许帮助。