Spring资源定位和载入
一、资源(Resource)
资源就是我们程序需要得到的信息,这些信息通常都是以各式各样的文件的形式存在。有二进制的、文本的、加密的,或者本地的、网络的,从不同的维度可以分成很多中类型。Spring中为了我们提供一个统一的资源接口Resource,它提供了访问资源的统一操作,并且为我们提供了一些资源的默认实现类,如下所示:
我个人理解,资源定位就是将各种资源文件封装成相应的Resource类,这样我们可以通过Resource接口提供的操作,对资源进行统一的访问。
我们在使用XmlBeanFactroy或者DefaultListableBeanFactory的时候,首先需要定义一个Resource来定位容器使用的BeanDefinition。这是使用ClassPathResource,这意味着Spring会在类路径中去寻找以文件形式存在的BeanDefinition。
ClassPathResource res = new ClassPathResource("bean.xml");
这里定义的Resource并不能由XmlBeanFactory或者DefaultListableBeanFactory直接使用,Spring通过BeanDefinitionReader来对这些信息进行处理。从这里我们可以看出ApplicationContext相对这些容器的好处了,因为在ApplicationContext中,Spring已经为我们提供了一系列加载不同Resource的读取器的实现,而XmlBeanFactory或者DefaultListableBeanFactory只是一个纯粹的IoC容器,需要为它配置特定的读取器才可以。当然,有利有弊,使用XmlBeanFactory或者DefaultListableBeanFactory这些更底层的容器,能提高定制IoC容器的灵活性。
下面就以实现ApplicationContext的实现类来谈谈Spring为我们提供的这些容器中是如何来进行资源定位和加载的。
二、资源定位
Spring为我们提供了许多默认的ApplicationContext的实现类,如FileSystemXmlApplicationContext、ClassPathXmlApplicationContext以及XmlWebApplicationContext等。简单从这些名字上看,可以清楚的看到他们都分别提供了哪些不同Resource的读入功能,FileSystemXmlApplicationContext可以从文件系统载入Resource,ClassPathXmlApplicationContext可以从Class Path载入Resource,XmlWebApplicationContext可以从web容器中载入Resource等等。下面我们给出有关ApplicationContext继承体系的类图。
资源的定位以及载入是发生在何时的?以FileSystemXmlApplicationContext为例,我们具体谈谈ApplicationContext容器的实现类是在何时、如何定位以及加载资源的。从FileSystemXmlApplicationContext的构造函数开始,不管是调用哪个构造函数,FileSystemXmlApplicationContext都会最终调用到下面构造函数
public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException { super(parent); //配置文件位置 setConfigLocations(configLocations); if (refresh) { //调用AbstractApplicationContext中默认的实现方法,refresh()实现了容器初始化的过程 refresh(); } }
Spring提供的ApplicationContext的大多数默认实现类(除了GenericApplicationContext这个继承分支的类除外,这个类将在最后做单独说明)的构造函数中最终都会调用到refresh(),refresh()是AbstractApplicationContext提供的一个默认实现,它实现了容器初始化所经历的一系列的操作,接下来我们就来看看这个方法的具体实现。
public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { // Destroy already created singletons to avoid dangling resources. beanFactory.destroySingletons(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } } }
资源的定位和载入就发生在obtainFreshBeanFactory()方法里面,这这个方法里面会调用到一个模板方法refreshBeanFactory(),这个方法由实现了ApplicationContext的子类去提供,以用于定制自己想要的BeanFactroy,在FileSystemXmlApplicationContext所调用的是处于这个继承分支上其父类AbstractXmlApplicationContext所提供的默认实现,如下:
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws IOException { // Create a new XmlBeanDefinitionReader for the given BeanFactory. //创建一个读取器 XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); // Configure the bean definition reader with this context's // resource loading environment. //未读取器指定资源加载器,因为ApplicationContext是继承了DefaultResourceLoader,所以这里参数未this beanDefinitionReader.setResourceLoader(this); beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); // Allow a subclass to provide custom initialization of the reader, // then proceed with actually loading the bean definitions. //允许子类去对加载器做一些定制,默认实现中什么都没做 initBeanDefinitionReader(beanDefinitionReader); //调用本类中的loadBeanDefinitions(XmlBeanDefinitionReader reader) loadBeanDefinitions(beanDefinitionReader); }
从上面方法可以知道,在容器初始化的过程中容器会创建一个读取器用来读取相应的Resource,那么具体是怎么定位以及读取的,还需要接着看loadBeanDefinitions(XmlBeanDefinitionReader reader)中所做的工作了
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { //getConfigResources()是个模板方法,用于获得配置的Resources,默认实现放回null,可以在子类中重写这个方法为配置Resources做定制,ClassPathXmlApplicationContext就实现了这个方法用来获取配置的Resources Resource[] configResources = getConfigResources(); if (configResources != null) { //由于直接指定了Resources,因此将文件定位为Resources的过程就不用了,直接调用AbstractBeanDefinitionReader提供的默认实现方法loadBeanDefinitions(Resource[] resources)载入资源 reader.loadBeanDefinitions(configResources); } //同上,getConfigLocations()这个方法也可以被子类重写,做定制处理。当然也可以直接利用默认实现做定制处理,查看下面的默认实现方法。 String[] configLocations = getConfigLocations(); if (configLocations != null) { //这里的参数是String[]类型的资源,需要定位成Resources供加载,因此相对于上面直接加载,这里会调用AbstractBeanDefinitionReader提供的默认实现方法loadBeanDefinitions(String[] locations),这个方法会先定位资源,然后在与上面那种情况一样,再调用loadBeanDefinitions(Resource[] resources)载入资源 reader.loadBeanDefinitions(configLocations); } } protected String[] getConfigLocations() { //可以重写getDefaultConfigLocations()方法做定制处理,XmlWebApplicationContext中就重写了该方法。 return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations()); }
我们接下来先看看资源是如何定位的吧,loadBeanDefinitions(String[] locations)中的方法很简单,就是循环这个String[],调用loadBeanDefinitions(String location)
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException { //资源定位的过程就发生在下面这个方法中 return loadBeanDefinitions(location, null); }
那我们来详细看看AbstractBeanDefinitionReader提供的loadBeanDefinitions(String location, Set actualResources)方法
public int loadBeanDefinitions(String location, Set actualResources) throws BeanDefinitionStoreException { ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader == null) { throw new BeanDefinitionStoreException( "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available"); } //如果资源加载器实现了自己的资源路径解析器,那么按照实现的模式定位资源 if (resourceLoader instanceof ResourcePatternResolver) { // Resource pattern matching available. try { //定位资源 Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); //载入资源 int loadCount = loadBeanDefinitions(resources); if (actualResources != null) { for (int i = 0; i < resources.length; i++) { actualResources.add(resources[i]); } } if (logger.isDebugEnabled()) { logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]"); } return loadCount; } catch (IOException ex) { throw new BeanDefinitionStoreException( "Could not resolve bean definition resource pattern [" + location + "]", ex); } } else { // Can only load single resources by absolute URL. //定位资源 Resource resource = resourceLoader.getResource(location); //载入资源 int loadCount = loadBeanDefinitions(resource); if (actualResources != null) { actualResources.add(resource); } if (logger.isDebugEnabled()) { logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]"); } return loadCount; } }
如何实现了自己的资源路径解析器的classLoader将调用自己实现的getResource(String location)方法,在这里就不多说了,我们来看看为实现自己的资源路径解析器的classLoader是如何定位的。跟踪代码我们可以看到,这个时候将调用DefaultResourceLoader这个Spring提供的ClassLoader的默认实现类中的getResource(String location)方法。
public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); //是不是classpath资源 if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { //不是classpath资源,那么尝试构造成一个网络资源 try { // Try to parse the location as a URL... URL url = new URL(location); return new UrlResource(url); } //既不是classpath资源,又不是网络资源,将调用用户自己的处理方式 catch (MalformedURLException ex) { // No URL -> resolve as resource path. //这是一个模板方法,在子类中重写该方法,将根据用户的方式进行资源定位 return getResourceByPath(location); } } }
从上面代码可以看到,Spring提供的默认资源加载器DefaultResourceLoader,会首先判断是不是classpath或者url资源,不过不是,将调用用户自己的getResourceByPath(location),不管哪种资源,最后返回的都是统一的资源接口Resource供程序调用。那我们在具体啊看看FileSystemXmlApplicationContext是如何重写getResourceByPath(String location)方法的吧。从名字来看,FileSystemXmlApplicationContext实现的getResourceByPath(String path)方法应该返回一个文件系统的资源Resource,从下面代码我们可以看到这个方法实现,最后retrun的确实是一个FileSystemResource。
protected Resource getResourceByPath(String path) { if (path != null && path.startsWith("/")) { path = path.substring(1); } //FileSystemXmlApplicationContext中得到的是一个FileSystemResource return new FileSystemResource(path); }
三、资源载入
我们同样接着上面的例子来讲,我们可以看到,最后资源加载都会调用到读取器中的loadBeanDefinitions(Resource resource)方法,从我们给的例子FileSystemXmlApplicationContext中可以看到,Spring为FileSystemXmlApplicationContext提供的读取器是XmlBeanDefinitionReader,因此最终会调用到我们提供的这个读取器的loadBeanDefinitions(Resource resource)放进行资源载入。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { return loadBeanDefinitions(new EncodedResource(resource)); }
我们继续看调用的这个方法
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); if (logger.isInfoEnabled()) { logger.info("Loading XML bean definitions from " + encodedResource.getResource()); } Set currentResources = (Set) this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet(4); this.resourcesCurrentlyBeingLoaded.set(currentResources); } if (!currentResources.add(encodedResource)) { throw new BeanDefinitionStoreException( "Detected recursive loading of " + encodedResource + " - check your import definitions!"); } try { //通过Resource提供的统一接口,将资源读取为InputStream来进行处理 InputStream inputStream = encodedResource.getResource().getInputStream(); try { //将流转换成InputSource进行处理 InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } //对读取的资源进行处理(解析,bean的注册等) return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); } finally { inputStream.close(); } } catch (IOException ex) { throw new BeanDefinitionStoreException( "IOException parsing XML document from " + encodedResource.getResource(), ex); } finally { currentResources.remove(encodedResource); if (currentResources.isEmpty()) { this.resourcesCurrentlyBeingLoaded.set(null); } } }
从上面可以看到,资源都通过统一的资源接口Resource提供的操作,读为InputStream流进行处理的。然后接下来的工作就完全是对读取的数据进行解析、bena的注册等后续操作了。