[Spring] 深度解析系列 (1 )- 资源定位

bean 的声明方式,已经在概述中描述,此处就不再赘述, 声明成概述的样子,就已经能满足我们的大多数应用了。

在文章开始是之前,先上一段最精简的代码,本篇文章都是围绕下面这段代码进行的,抽丝拨茧留下的才是精华。

如下:

//首先进行BeanDefinition资源文件的定位,封装为Source的子类
ClassPathResource res = new ClassPathResource("application.xml");
//创建基本的IOC容器
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
//创建文件读取器,并进行回调设置。
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
//资源加载解析
reader.loadBeanDefinitions(res);
MessageService bean = factory.getBean(MessageService.class);
String message = bean.getMessage();
System.out.println(message);

上面的代码,打破了代码的封装,直接穿透到了底层,执行流程是否是更加的清晰了。为了让读者有一个更清晰的认识,我特地画了一张流程 图。首先有一个宏观上的把控,这也是本篇文章的叙述线索。没有接触过源码的同学,此时看这张 图可能 还是有点模糊,不过没有关系,后面我会进行更加详细的分析,建议读者同学,在阅读文章 的同时,更跟随一下本地代码的执行,这样流程能更加的清晰。
流程图如下:

资源定位执行流程.png

图片有些失真,看不太清,没关系,上传到了github, 点我就可以

启动过程分析

接下来,从ClassPathXmlApplicationContext 的构造函数开始,分析定义Bean的资源文件解析过程。有些人会用FileSystemXmlApplicationContext 作为入口,这里我使用的是ClassPathXmlApplication。 这个 没有关系,无论以哪个类最为入口最终都会流向AbstractApplicationContext 的refresh()方法,效果都是一样的。

为了不脱离主线和节省篇幅,后边的流程我只截取最主干的部分就行解析。ClassPathApplicationContext 提供了多个构造函数,但是最终都会转到下面的方法来执行。

image.png

此构造方法的代码并不是很多,只有四行,并且在该方法中显示的使用了super 关键字,其实在类初始化的过程中,super关键字并不是必须的,编译器会默认帮我们添加上,只不过执行的是无参的构造方法,这里要执行父类参数为ApplicationContext类型的构造方法,才进行了显示的声明。

来看一下上面的构造方法中,最终都执行了那些操作,具体的操作都是由那些类执行的。
通过代码跟随,可以发现,super(parent)这个方法最终是由其基类(AbstractApplicationContext)来执行,执行了AbstractApplicationContext的无参构造方法和setParent()方法。其代码如下,省略了静态代码块的定义:

image.png

调用父类的构造方法执行完成后,返回ClassPathApplicationContext类,执行setConfigLocations(configLocations)方法,该方法的定义在类AbstractRefreshableConfigApplicationContext中:

image.png

在上面的方法中,看到参数采用的数组的方式,也就是说,资源文件支持下面两种实现方式,扩展性更好一些:

  • ClasspathResource res = new ClasspathResource("a.xml,b.xml");
  • ClasspathResource res = new ClasspathResource("new String(){'a.xml' , 'b.xml'}")

我们来看以上,程序执行到此处后,都做了哪些操作:

  1. 在AbstractApplicationContext中初始化了resourcePatternResolver(多资源文件的载入),用于获取Resource,关于何时使用后面再解释
  2. 将资源的定义路径保存在了configLocations数组中

到此,IOC容器根据资源定义路径获取Resouce的准备工作便完成了。

下面来看一下ClassPathXmlApplicationContext 中的关于refresh()方法的调用,实际调用的是AbstractApplicationContext中的refresh()方法,该方法定义如下:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        //环境准备,获取容器启动的时间,设置活动标志,以及属性的初始化
        prepareRefresh();
        //在子类中启动resreshBeanfactory()方法
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        //为BeanFactory配置容器属性
        prepareBeanFactory(beanFactory);
        try {
            //设置BeanFactory的后置处理
            postProcessBeanFactory(beanFactory);
            //调用后置处理,为这些后置处理器在Bean的定义中向容器注册
            invokeBeanFactoryPostProcessors(beanFactory);
            //注册Bean的后续处理,在Bean创建过程中调用
            registerBeanPostProcessors(beanFactory);
            //初始化上下文消息机制
            initMessageSource();
            //初始化上下文中事件机制
            initApplicationEventMulticaster();
            //初始化其他特殊的Bean
            onRefresh();
            //检查监听Bean并且将这些Bean向容器注册
            registerListeners();
            //初始化所有的singleton beans , 设置lazy-init = true 的bean除外
            finishBeanFactoryInitialization(beanFactory);
            //发布容器事件,结束Refresh过程
            finishRefresh();
        }catch (BeansException ex) {
            // 为防止Bean资源占用,在异常处理中,销毁已经生成的单件Bean
            destroyBeans();
            //重置Rest标志
            cancelRefresh(ex);
            // Propagate exception to caller.
            throw ex;
        }
    }
}

上面的代码便是整个执行的流程。可以看到使用了synchronized 关键字,防止没有初始化 完成,再次进行初始化。

refresh()方法主要为IOC容器Bean的生命周期提供管理条件。Spring IOC容器的生成是从refreshBeanFactory()方法开始的,也就是执行了下面的代码:


image.png

在AbstractApplicationContext抽象类中,只是进行了refreshBeanFactory()方法的定义,方法的实现是在其子类AbstractRefreshableApplicationContext中实现的,在子类的定义如下:


image.png

上面代码loadBeanDefinitions(beanFactory);我们说是一个委派模式,只是进行了方法的定义,具体实现则是由AbstractXmlApplicationContext类实现,在该方法中创建了读取器XmlBeanDefinitionReader的实例,然后把这个读取器在IOC容器中设置好,最后是启动读取器来完成对BeanDefinition在IOC容器中的载入,定义如下:


image.png

在XmlBeanDefinitionReader的初始化过程中,还进行了一些其他的操作,具体如下:
image.png

通过上面代码发现,在创建XmlBeanDefinitionReader的过程中,完成了resourceLoader和eviironment的赋值操作。

首先得到BeanDefinition信息的Resource定位,然后直接调用XmlBeanDefinitionReader来读取,具体的载入过程是委托给BeanDefinitionReader来完成的。因为使用的FileSystemXmlApplicationContext, getConfigResources()方法返回的是null,所以程序会走第二个分支

 //Xml Bean读取器加载Bean定义资源
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
    //获取资源定位
    Resource[] configResources = getConfigResources();
    if (configResources != null) {
        //Xml Bean读取器调用其父类AbstractBeanDefinitionReader读取定位 
        reader.loadBeanDefinitions(configResources);
    }
    //如果子类获取的Bean定义为空,则获取FileSystemXmlApplication构造方法中setConfigLocations方法设置的资源。
    String[] configLocations = getConfigLocations();
    if (configLocations != null) {
        reader.loadBeanDefinition(configLocations);
    }
}

程序分析到这,来梳理一下上面的执行流程。

在ClassPathXmlApplicationContext 一共做了三件事

调用了父类的构造器,进行了初始化
设置了BeanDefinition的定义路径
执行了Refresh()方法
refresh()方法来启动整个BeanDefinition的载入过程

创建容器 DefaultListableBeanFactory
创建了XmlBeanDefinitionReader
开始准备通过reader来加载资源
AbstractBeanDefinitionReader读取Bean定义资源,AbstractBeanDefinitionReader的loadBeanDefinitions方法源码如下:

//方法重载
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
    Assert.notNull(locations, "Location array must not be null");
    int counter = 0;
    String[] var3 = locations;
    int var4 = locations.length;
    for(int var5 = 0; var5 < var4; ++var5) {
        String location = var3[var5];
        counter += this.loadBeanDefinitions(location);
    }
    return counter;
}
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
    return this.loadBeanDefinitions(location, (Set)null);
}
//重载的最终执行方法
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
    //获取资源加载器
    //上面提到过,XmlBeanDefinitionReader初始化时,在其父类中执行了加载器的初始化操作
    //resourceLoader的类型为PathMatchingResourcePatternResolver
    ResourceLoader resourceLoader = getResourceLoader();
    if (resourceLoader == null) {
        throw new BeanDefinitionStoreException("Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
    }
    //判断类型
    if (resourceLoader instanceof ResourcePatternResolver) {
        try {
            //将指定位置的Bean定义资源文件解析转化为Resource
            //加载多个指定位置的资源定义文件
            Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
            //读取Resource
            int loadCount = loadBeanDefinitions(resources);
            if (actualResources != null) {
                for (Resource resource : resources) {
                    actualResources.add(resource);
                }
            }
            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;
    }
}

public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
        Assert.notNull(resources, "Resource array must not be null");
        int counter = 0;
        Resource[] var3 = resources;
        int var4 = resources.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Resource resource = var3[var5];
            counter += this.loadBeanDefinitions((Resource)resource);
        }

        return counter;
    }

上面的方法主要进行了两件事:

调用资源加载器获取资源 resourceLoader.getResource(location)
真正执行加载功能的是子类XmlBeanDefinitionReader的loadBeanDefinitions方法
loadBeanDefinitions()方法在AbstractBeanDefinitionReader中并没有具体的实现,它会转到XmlBeanDefinitionReader中的loadBeanDefinitions(Resource resource)中运行:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    //对xml资源进行编码处理
    return this.loadBeanDefinitions(new EncodedResource(resource));
}
//方法重载,转入此方法执行
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if(this.logger.isInfoEnabled()) {
        this.logger.info("Loading XML bean definitions from " + encodedResource.getResource());
    }
    Object currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();
    if(currentResources == null) {
        currentResources = new HashSet(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    if(!((Set)currentResources).add(encodedResource)) {
        throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    } else {
        int var5;
        try {
            //将资源文件转换为类型为InputStream的I/O流
            InputStream ex = encodedResource.getResource().getInputStream();
            try {
                //从InputStream中得到xML的解析源
                InputSource inputSource = new InputSource(ex);
                //编码如果不为null, 则设置inputSource的编码
                if(encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                //读取数据
                var5 = this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            } finally {
                ex.close();
            }
        } catch (IOException var15) {
            throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), var15);
        } finally {
            ((Set)currentResources).remove(encodedResource);
            if(((Set)currentResources).isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
        return var5;
    }
}

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)throws BeanDefinitionStoreException {
        try {
            //转化为Document 对象
            Document doc = doLoadDocument(inputSource, resource);
            //启动对Bean定义解析的详细过程,会用到Spring Bean的配置规则
            return registerBeanDefinitions(doc, resource);
        }
        //删除了部分catch语句
        catch (....) {
            throw ex;
        }
    }

将XML文件转换成Document对象,解析过程由documentLoader实现。到此就完成了xml文件的读取工作。
下面详细分析一下,文件是如何读取的? , 回到下面的这段代码

 //Xml Bean读取器加载Bean定义资源
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
    //获取资源定位
    Resource[] configResources = getConfigResources();
    if (configResources != null) {
        //Xml Bean读取器调用其父类AbstractBeanDefinitionReader读取定位 
        reader.loadBeanDefinitions(configResources);
    }
    //如果子类获取的Bean定义为空,则获取FileSystemXmlApplication构造方法中setConfigLocations方法设置的资源。
    String[] configLocations = getConfigLocations();
    if (configLocations != null) {
        reader.loadBeanDefinition(configLocations);
    }
}

程序有两个分支,一个是获取Resource[] 数组的分支,一个是 String[]数组的分支。在创建ClassPathXmlApplicationContext 的时候,初始化了getConfigLocations()方法,所以程序会走第二个分支。configLoactions的内容就是 classpath:application.xml .之后程序进入到XmlBeanDefinitionReader中将 文件转换为流。

运行 AbstractBeanDefinitionReader中的 loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) ,这段代码很长,我只是截取了部分内容 ,定义如下:

image.png

getResourceLoader()方法,返回了 ClassPathXmlApplicationContext ,ClassPathXmlApplicationContext 是 ResourcePatternResolver的子类。所以会执行 Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); 此处代码就是资源定位的核心。 PathMatchingResourcePatternResolver 是 ResourcePatternResolver 的实现类 ,最终会执行 它的getResource()方法。


image.png

经过一系列判断,最终程序会走 return new Resource[] {getResourceLoader().getResource(locationPattern)}; 这段代码 ,getResourceLoader()返回的是DefaultResourceLoader 类。执行的是它的getResource()方法,方法定义如下:


image.png

程序最终是由这段代码,进行文件的加载:

return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());

getClassLoader()返回的是 AppClassLoader .

ClassPathResource 作为Resource的子类,自然可以得到 Resource[]类型的数组。那么只要调用 getInputStream()方法,便可以得到文件流。
那么下面看一下,是否是这样实现的。

下面的代码我就直接罗列了, 没有什么太复杂的逻辑。

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
                # 在进行一个封装
        return loadBeanDefinitions(new EncodedResource(resource));
    }
image.png

和预想的一样,最后调用了getInputStream()方法,使用了InputStream将流转换成字节流。既然有了流,就可以创建Document了。以便来解析文件中定义的内容。

既然分析到这了,就多说一点,上面的文件加载最终是通过ClassLoader加载的。
ClassLoader: 负责加载类的对象。是一个抽象类。通过一个指定类的全限定名,找到对应的Class字节码文件,然后加载它并转化成一个java.lang.Class类的实例。
ClassLoader 分类:

  • 启动类加载器: 负责加载\lib 目录下的类加载到虚拟机内存中
  • 扩展类加载器:这个类负责加载\lib\ext目录下的类库
  • 应用类加载器:这个类加载器负责加载用户类路径 classpath 下的类库,一般自己编写的类都是由这个加载器进行加载的。

除了上面的三种加载器,还可以自定义类加载器,不过一般用不到。
说到类加载器自然离不开双亲委派模型- 它的工作原理是:如果一个类加载器收到类的加载请求,不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完整,这样层层递进,最后所有的请求被传到顶层的类加载器。只有当父类加载器无法加载,才会交给子类加载器去加载。这样的好处是防止类的重复加载。

ClassLoader是从classPath中读取资源的一个类,一般都是用来加载class, 实际上,但凡是处在classpath中的文件,统称为资源,都可以是classLoader来读取。推荐使用此种方式Thread.currentThread().getContextClassLoader() 获取classLoader , 尽量少用 ClassLoader.getSystemClassLoader()的方式。

本篇文章就到这,下次来写xml文件的读取和标签的解析。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,809评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,189评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,290评论 0 359
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,399评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,425评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,116评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,710评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,629评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,155评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,261评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,399评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,068评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,758评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,252评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,381评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,747评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,402评论 2 358

推荐阅读更多精彩内容