Spring资源定位和载入

一、资源(Resource)

        资源就是我们程序需要得到的信息,这些信息通常都是以各式各样的文件的形式存在。有二进制的、文本的、加密的,或者本地的、网络的,从不同的维度可以分成很多中类型。Spring中为了我们提供一个统一的资源接口Resource,它提供了访问资源的统一操作,并且为我们提供了一些资源的默认实现类,如下所示:


Spring资源定位和载入

         我个人理解,资源定位就是将各种资源文件封装成相应的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继承体系的类图。


Spring资源定位和载入
         资源的定位以及载入是发生在何时的?以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的注册等后续操作了。

相关推荐