现在的位置: 首页 > 综合 > 正文

读懂Spring核心系列4(XML文件配置)

2018年05月28日 ⁄ 综合 ⁄ 共 6436字 ⁄ 字号 评论关闭

回顾上一篇的内容,经过3个系列的累积,我们列出的代码已经能够自动装配bean。但是美中不足的是,这些bean的类路径以及属性都是手动编写代码才能添加到容器中的。在Spring的实现中,会使用XML文档来配置我们需要的信息。所以这一次,我们结合上一篇给出的代码,将要实现使用XML来进行信息的配置。

在实现的整个过程中,大致分为3个步骤:1、找到资源,2、读取资源,3、将读取的数据注入容器。

首先需要定义资源,在一个使用Spring的程序中,配置的资源可以包括XML,properties等配置文件。所以,首先我们定义一个面向资源的接口,统一管理资源。

/**
 * Resource是spring内部定位资源的接口。
 * @author yihua.huang@dianping.com
 */
public interface Resource {

    InputStream getInputStream() throws IOException;
}

接下来就需要实现上面这个接口了,实现接口的子类需要标识资源,并且实现接口中的方法,得到输入流对象。

/**
 * @author yihua.huang@dianping.com
 */
public class UrlResource implements Resource {

    private final URL url;

    public UrlResource(URL url) {
        this.url = url;
    }

    @Override
    public InputStream getInputStream() throws IOException{
        URLConnection urlConnection = url.openConnection();
        urlConnection.connect();
        return urlConnection.getInputStream();
    }
}

在这个类中,我们使用URL这个统一资源定位符来标识我们需要读取的资源。通过URL对象打开连接,我们就能读取到输入流啦。

那么上面提及的这个URL又是怎么获取到的呢?当然,我们可以指定XML文件的绝对路径来作为URL,但是这样就得依赖具体的操作系统平台啦。很明显这样的设想是不够理想的,所以我们需要一个资源加载类来专门获取XML并标识为URL。

/**
 * @author yihua.huang@dianping.com
 */
public class ResourceLoader {

    public Resource getResource(String location){
        URL resource = this.getClass().getClassLoader().getResource(location);
        return new UrlResource(resource);
    }
}

这个资源加载类能够返回一个UrlResource对象,并且已经实例化了UrlResource对象里面的URL。看一下它是怎么得到URL的:通过获取自己的类加载器来加载资源。location只需要指定XML文件的名称就可以了。类加载器将会在classpath指定的路径下查找名称为location的文件,并且加载它。那么,我们为Spring添加的XML配置文件就必须位于classpath指定的路径下了,不然类加载器是找不到XML文件的。

下面是资源加载的测试代码

/**
 * @author yihua.huang@dianping.com
 */
public class ResourceLoaderTest {

	@Test
	public void test() throws IOException {
	ResourceLoader resourceLoader = new ResourceLoader();
        Resource resource = resourceLoader.getResource("tinyioc.xml");
        InputStream inputStream = resource.getInputStream();
        Assert.assertNotNull(inputStream);
    }
}

测试通过则说明文件已经加载成功了,我们已经取到它的输入流对象。

到这一步,“找到资源”已经完成了,下面就需要“读取资源”了。同样,我们用一个接口来抽象读取资源的操作,这个接口以后会被多个子类实现,因为读取不同的文件当然要不同的读取方式,不同的子类将具体的实现不同的方式。

/**
 * 从配置中读取BeanDefinitionReader
 * @author yihua.huang@dianping.com
 */
public interface BeanDefinitionReader {

    void loadBeanDefinitions(String location) throws Exception;
}

下面是实现上面接口的子类,但是注意,在下面这个类中仍然没有实现接口中的方法。第一,它只是增加了一个Map成员变量,这个Map将负责存储读取到的资源数据,hash的key作为读取到bean的id,hash的值则保存具体的数据,包括bean的类路径,名称,class对象,属性值等。BeanDefinition这个对象在前几篇就出现了,可以在前面找到它的定义。第二,增加ResourceLoader,这个对象可以取到输入流,为读取做准备。最后,新增的几个方法相信都一目了然啦。

说到底,下面这个类这是一个过渡类,它为具体的读取做一些准备而已。你也可以看做实现不同读取方式的子类抽出来的公有方法了。

/**
 * 从配置中读取BeanDefinitionReader
 * 
 * @author yihua.huang@dianping.com
 */
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

    private Map<String,BeanDefinition> registry;

    private ResourceLoader resourceLoader;

    protected AbstractBeanDefinitionReader(ResourceLoader resourceLoader) {
        this.registry = new HashMap<String, BeanDefinition>();
        this.resourceLoader = resourceLoader;
    }

    public Map<String, BeanDefinition> getRegistry() {
        return registry;
    }

    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }
}

接下来的类继承上面的类,它终于要具体的实现读取的方法了,也是本篇中最长的类了。先贴代码,代码后面将作简短的说明。

/**
 * @author yihua.huang@dianping.com
 */
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

	public XmlBeanDefinitionReader(ResourceLoader resourceLoader) {
		super(resourceLoader);
	}

	@Override
	public void loadBeanDefinitions(String location) throws Exception {
		InputStream inputStream = getResourceLoader().getResource(location).getInputStream();
		doLoadBeanDefinitions(inputStream);
	}

	protected void doLoadBeanDefinitions(InputStream inputStream) throws Exception {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder = factory.newDocumentBuilder();
		Document doc = docBuilder.parse(inputStream);
		// 解析bean
		registerBeanDefinitions(doc);
		inputStream.close();
	}

	public void registerBeanDefinitions(Document doc) {
		Element root = doc.getDocumentElement();

		parseBeanDefinitions(root);
	}

	protected void parseBeanDefinitions(Element root) {
		NodeList nl = root.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			if (node instanceof Element) {
				Element ele = (Element) node;
				processBeanDefinition(ele);
			}
		}
	}

	protected void processBeanDefinition(Element ele) {
		String name = ele.getAttribute("name");
		String className = ele.getAttribute("class");
        BeanDefinition beanDefinition = new BeanDefinition();
        processProperty(ele,beanDefinition);
        beanDefinition.setBeanClassName(className);
		getRegistry().put(name, beanDefinition);
	}

    private void processProperty(Element ele,BeanDefinition beanDefinition) {
        NodeList propertyNode = ele.getElementsByTagName("property");
        for (int i = 0; i < propertyNode.getLength(); i++) {
            Node node = propertyNode.item(i);
            if (node instanceof Element) {
                Element propertyEle = (Element) node;
                String name = propertyEle.getAttribute("name");
                String value = propertyEle.getAttribute("value");
                beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name,value));
            }
        }
    }
}

方法的调用从上到下,其实非常清晰了。这里简短描述下过程。先贴一个XML文件示例。

<beans>
    <bean name="helloWorldService" class="us.codecraft.tinyioc.HelloWorldService">
        <property name="text" value="Hello World!"></property>
    </bean>
</beans>

我把文件的命名空间之类的删除了,保持简洁会容易看一些。

1、loadBeanDefinitions方法取到了输入流,调用doLoadBeanDefinitions。

2、doLoadBeanDefinitions构建出DOM对象,关闭输入流,调用registerBeanDefinitions。

3、registerBeanDefinitions取到根节点,这个对应上面XML的beans节点,然后调用parseBeanDefinitions。

4、parseBeanDefinitions遍历子节点,找到bean节点,调用processBeanDefinition。

5、processBeanDefinition取到bean节点的name属性作为id,class属性作为类全名,调用processProperty构建BeanDefinition。

6、processProperty取到bean节点的子节点,也就是property节点啦,然后读取到属性名称与值。

到此为止,“读取资源”已经完成了,测试的代码如下:

/**
 * @author yihua.huang@dianping.com
 */
public class XmlBeanDefinitionReaderTest {

	@Test
	public void test() throws Exception {
		XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(new ResourceLoader());
		xmlBeanDefinitionReader.loadBeanDefinitions("tinyioc.xml");
		Map<String, BeanDefinition> registry = xmlBeanDefinitionReader.getRegistry();
		Assert.assertTrue(registry.size() > 0);
	}
}

最后“将读取的数据注入容器”已经非常简单了,因为上几篇文章里面已经完成了注入容器的过程了。看一看测试的代码就明白了。

/**
 * @author yihua.huang@dianping.com
 */
public class BeanFactoryTest {

	@Test
	public void test() throws Exception {
		// 1.读取配置
		XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(new ResourceLoader());
		xmlBeanDefinitionReader.loadBeanDefinitions("tinyioc.xml");

		// 2.初始化BeanFactory并注册bean
		BeanFactory beanFactory = new AutowireCapableBeanFactory();
		for (Map.Entry<String, BeanDefinition> beanDefinitionEntry : xmlBeanDefinitionReader.getRegistry().entrySet()) {
			beanFactory.registerBeanDefinition(beanDefinitionEntry.getKey(), beanDefinitionEntry.getValue());
		}

		// 3.获取bean
		HelloWorldService helloWorldService = (HelloWorldService) beanFactory.getBean("helloWorldService");
		helloWorldService.helloWorld();

	}
}

测试代码中第2步的循环操作就注入容器了。

到此为止,本篇需要完成的目标就实现了。这下子有了XML配置文件,终于有点像正经的Spring了。老惯例在最后给出UML相关代码的UML。因为上一篇给出的UML已经包括了之前的代码,今天的UML就只包括本篇中新出现的代码啦。

抱歉!评论已关闭.