Spring IoC之ClassPathXmlApplicationContext

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: Spring IoC之ClassPathXmlApplicationContext

概述


前面我们讲了 Spring 资源的定义和资源的加载情况,还剩下 BeanFactory、BeanDefinition、BeanDefinitionReader 和 ApplicationContext 需要学习,通常情况下,这些内容关联性比较紧密,所以我们从实际应用场景中对 bean 的加载流程有个大概的认识,然后再具体到每一个知识点进行讲解。


在之前的文章中加载 Bean 的时候采用的是  ApplicationContext, ApplicationContext 和 BeanFactory 两者都是用于加载 Bean 的,但是相比之下,ApplicationContext 提供了更多的扩展功能 ,简而言之: ApplicationContext 包含 BeanFactory 的所有功能。通常建议优先使用  ApplicationContext。


那么究竟  ApplicationContext 比 BeanFactory 多了哪些功能?首先我们来看看使用两个不同的类去加载配置文件在写法上有何不同:


//使用BeanFactory方式加载XML.虽然XmlBeanFactory在Spring 3.1中被标记为不建议使用,但是不影响我们分析源码
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("application_context.xml"));
//使用ApplicationContext方式加载XML.
ApplicationContext bf = new ClassPathXmlApplicationContext("application_context.xml");
复制代码


接下来我们就以  ClassPathXmlApplicationContext 作为切入点,对整体功能进行分析。


ClassPathXmlApplicationContext


为了后面便于代码阅读,先给出一下 ClassPathXmlApplicationContext 这个类的继承关系 :


image.png


可以看出该类是  ApplicationContext 和  BeanFactory 的子接口。


构造函数

public ClassPathXmlApplicationContext() {
}
public ClassPathXmlApplicationContext(ApplicationContext parent) {
    super(parent);
}
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
    this(new String[]{configLocation}, true, (ApplicationContext)null);
}
public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {
    this(configLocations, true, (ApplicationContext)null);
}
public ClassPathXmlApplicationContext(String[] configLocations, @Nullable ApplicationContext parent) throws BeansException {
    this(configLocations, true, parent);
}
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException {
    this(configLocations, refresh, (ApplicationContext)null);
}
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
    super(parent);
    this.setConfigLocations(configLocations);
    if (refresh) {
        this.refresh();
    }
}
public ClassPathXmlApplicationContext(String path, Class<?> clazz) throws BeansException {
    this(new String[]{path}, clazz);
}
public ClassPathXmlApplicationContext(String[] paths, Class<?> clazz) throws BeansException {
    this(paths, clazz, (ApplicationContext)null);
}
public ClassPathXmlApplicationContext(String[] paths, Class<?> clazz, @Nullable ApplicationContext parent) throws BeansException {
    super(parent);
    Assert.notNull(paths, "Path array must not be null");
    Assert.notNull(clazz, "Class argument must not be null");
    this.configResources = new Resource[paths.length];
    for(int i = 0; i < paths.length; ++i) {
        this.configResources[i] = new ClassPathResource(paths[i], clazz);
    }
    this.refresh();
}
复制代码


从上述构造函数的代码可以看出大致分为两种,构造方法A:public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)和构造方法B: public ClassPathXmlApplicationContext(String[] paths, Class<?> clazz, @Nullable ApplicationContext parent),它们的使用稍微有点区别:

ApplicationContext context = new ClassPathXmlApplicationContext("application_context.xml");
//经测试,第二个参数并无指定性的作用,所以就算换一个class也可以
ApplicationContext context = new ClassPathXmlApplicationContext("/application_context.xml", User.class);
复制代码


这两种构造方法从用法上来讲,肯定是构造方法A更方便,在调试过程中,发现两种方法在获取资源的方式有所不同,主要提现在这一部分:


public InputStream getInputStream() throws IOException {
    InputStream is;
    if (this.clazz != null) {
        is = this.clazz.getResourceAsStream(this.path);
    } else if (this.classLoader != null) {
        is = this.classLoader.getResourceAsStream(this.path);
    } else {
        is = ClassLoader.getSystemResourceAsStream(this.path);
    }
    if (is == null) {
        throw new FileNotFoundException(this.getDescription() + " cannot be opened because it does not exist");
    } else {
        return is;
    }
}
复制代码


构造方法A获取资源是通过 ClassLoader 获取的,而构造方法B是由 Class 获取的。

接下来我们先一步一步来分析构造方法A。


构造方法之configLocations


public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
    super(parent);
    this.setConfigLocations(configLocations);
    if (refresh) {
        this.refresh();
    }
}
复制代码


1、 super(parent)


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


//FileSystemXmlApplicationContext调用父类构造方法就是该方法
public AbstractApplicationContext(ApplicationContext parent) {
    this();
    setParent(parent);
}
//具体需要执行的方法
public AbstractApplicationContext() {
    this.resourcePatternResolver = getResourcePatternResolver();
}
//获取一个Source的加载器用于读入Spring Bean 定义资源文件
protected ResourcePatternResolver getResourcePatternResolver() {
    return new PathMatchingResourcePatternResolver(this);
}
//设置双亲ioc容器
public void setParent(ApplicationContext parent) {
    this.parent = parent;
    if (parent != null) {
        Environment parentEnvironment = parent.getEnvironment();
        if (parentEnvironment instanceof ConfigurableEnvironment) {
            getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
        }
    }
}
复制代码


截止目前, ClassPathXmlApplicationContext 已经有了两个资源加载器(一个是由  AbstractApplicationContext 继承 DefaultResourceLoader 而来 ;一个是  AbstractApplicationContext 主动创建的 PathMatchingResourcePatternResolver )。 DefaultResourceLoader 只能加载一个特定类路径的资源 ;


PathMatchingResourcePatternResolver 可以根据 Ant 风格加载多个资源。

super(parent);用来设置父级 ApplicationContext,这里是 null 。


@Test
public void MyBean(){
    //解析application_context.xml文件 , 生成管理相应的Bean对象
    ApplicationContext context = new ClassPathXmlApplicationContext("application_context.xml");
    System.out.println(context.getParent());//输出结果null
    User user = (User) context.getBean("user");
    System.out.println(user);
}
复制代码


2、设置文件配置路径


先看下面这个案例:

@Test
public void MyBean(){
    //解析application_context.xml文件 , 生成管理相应的Bean对象
    //        ApplicationContext context = new ClassPathXmlApplicationContext("application_context.xml");
    //自定义一个系统属性,名为 spring 值为配置文件全名
    System.setProperty("spring","application_context");
    //使用占位符设置配置文件路径
    ApplicationContext context = new ClassPathXmlApplicationContext("${spring}.xml");
    System.out.println(context.getParent());
    System.out.println(context.getEnvironment());
    System.out.println(context.getClassLoader());
    User user = (User) context.getBean("user");
    System.out.println(user);
}
复制代码


执行结果为:


null
StandardEnvironment {activeProfiles=[], defaultProfiles=[default], propertySources=[PropertiesPropertySource {name='systemProperties'}, SystemEnvironmentPropertySource {name='systemEnvironment'}]}
sun.misc.Launcher$AppClassLoader@18b4aac2
User{name='hresh'}
复制代码


上述案例的实现,就多亏了 setConfigLocations(configLocations)方法,在构造方法B中就无法这样使用,接下来我们就学习一下该方法。

setConfigLocations(configLocations)方法用来设置配置路径, 支持多个配置文件以数组方式同时传入 。 该方法的定义在类

AbstractRefreshableConfigApplicationContext中:

//处理资源定义的数组,解析Bean文件的定义路径
public void setConfigLocations(String[] locations) {
        if (locations != null) {
            Assert.noNullElements(locations, "Config locations must not be null");
            this.configLocations = new String[locations.length];
            for (int i = 0; i < locations.length; i++) {
                //解析路径
                this.configLocations[i] = resolvePath(locations[i]).trim();
            }
        }
        else {
            this.configLocations = null;
        }
    }
protected String resolvePath(String path) {
    return this.getEnvironment().resolveRequiredPlaceholders(path);
}
复制代码


其中如果给定的路径中包含特殊符号,如${var},那么会在方法 resolvePath 中解析系统变量并替换 。


接着分析this.getEnvironment()


public ConfigurableEnvironment getEnvironment() {
    if (this.environment == null) {
        this.environment = this.createEnvironment();
    }
    return this.environment;
}
protected ConfigurableEnvironment createEnvironment() {
    return new StandardEnvironment();
}
复制代码


首先 getEnvironment()获取 Environment:如果 Environment 为 null 则 new一个 StandardEnvironment 出来:

AbstractApplicationContext.createEnvironment()

而 StandardEnvironment 继承于 AbstractEnvironment,AbstractEnvironment 中包含 MutablePropertySources 属性:


public class StandardEnvironment extends AbstractEnvironment {
    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
    public StandardEnvironment() {
    }
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new PropertiesPropertySource("systemProperties", this.getSystemProperties()));
        propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", this.getSystemEnvironment()));
    }
}
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    private final MutablePropertySources propertySources = new MutablePropertySources();
    //PropertyResolver : Environment的顶层接口,主要提供属性检索和解析带占位符的文本。
    private final ConfigurablePropertyResolver propertyResolver;
    public AbstractEnvironment() {
        this.propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
        //这个抽象方法在StandardEnvironment中实现
        this.customizePropertySources(this.propertySources);
    }
    protected void customizePropertySources(MutablePropertySources propertySources) {
    }
    .....
}
复制代码


getSystemProperties():


public Map<String, Object> getSystemProperties() {
    try {
        return System.getProperties();
    } catch (AccessControlException var2) {
        return new ReadOnlySystemAttributesMap() {
            @Nullable
            protected String getSystemAttribute(String attributeName) {
                try {
                    return System.getProperty(attributeName);
                } catch (AccessControlException var3) {
                    if (AbstractEnvironment.this.logger.isInfoEnabled()) {
                        AbstractEnvironment.this.logger.info("Caught AccessControlException when accessing system property '" + attributeName + "'; its value will be returned [null]. Reason: " + var3.getMessage());
                    }
                    return null;
                }
            }
        };
    }
}
复制代码


System 类中实现了  System.getProperties() :


public static Properties getProperties() {
    SecurityManager var0 = getSecurityManager();
    if (var0 != null) {
        var0.checkPropertiesAccess();
    }
    return props;
}
复制代码


Properties 类:


public class Properties extends Hashtable<Object, Object>
复制代码


由此段代码可以看出 Properties 继承于 Hashtable,以键值对的方式实现。


回到上面的路径解析问题: this.getEnvironment().resolveRequiredPlaceholders(path)就是启动了AbstractEnvironment.resolveRequiredPlaceholders(path)


public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    return this.propertyResolver.resolveRequiredPlaceholders(text);
}
复制代码


进而转换为  AbstractEnvironment.propertyResolver.resolveRequiredPlaceholders(text) , 而 AbstractEnvironment.propertyResolver:


public AbstractEnvironment() {
    this.propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
    this.customizePropertySources(this.propertySources);
}
复制代码


PropertySourcesPropertyResolver 继承于 AbstractPropertyResolver,则最终触发 AbstractPropertyResolver.resolveRequiredPlaceholders(String text):


public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    //初始化占位符解析器
    if (this.strictHelper == null) {
        this.strictHelper = this.createPlaceholderHelper(false);
    }
    //调用doResolvePlaceholders,进行替换占位符具体值
    return this.doResolvePlaceholders(text, this.strictHelper);
}
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
    //替换占位符具体值
    return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}
复制代码


追溯 replacePlaceholders() 方法,定位到 PropertyPlaceholderHelper 类中:


public String replacePlaceholders(String value, PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver) {
    Assert.notNull(value, "'value' must not be null");
    return this.parseStringValue(value, placeholderResolver, (Set)null);
}
复制代码


查看代码一直没明白调用该方法时的参数值,this::getPropertyAsRawString 指的是 PropertySourcesPropertyResolver 类中的 getPropertyAsRawString 方法,返回值为 String 类型,但是进入 replacePlaceholders 方法中的却是 PlaceholderResolver 类型。在网上看到这么一段代码:


private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
                //替换占位符具体值
        return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {
            @Override
            public String resolvePlaceholder(String placeholderName) {
                                //通过key获取占位符对应的String类型具体值
                return getPropertyAsRawString(placeholderName);
            }
        });
    }
复制代码


如果是上述代码,倒是可以理解。如果有大神知道原因,请不吝赐教。


最后执行的 PropertyPlaceholderHelper 类中的 parseStringValue 方法,至此关于配置文件路径的解析就结束了。接下来就是读取配置文件、解析、注册 Bean。

扩展:关于  PropertyResolver  占位符解析,有以下代码示例:


@Test
public void PropertyResolverTest(){
    PropertySource propertySource = new MapPropertySource("source", Collections.<String, Object>singletonMap("name","hresh"));
    MutablePropertySources mutablePropertySources = new MutablePropertySources();
    mutablePropertySources.addFirst(propertySource);
    PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(mutablePropertySources);
    System.out.println(propertyResolver.getProperty("name"));//hresh
    System.out.println(propertyResolver.resolveRequiredPlaceholders("name is ${name}"));//hresh
}
复制代码


PropertyResolver 的默认实现是 PropertySourcesPropertyResolver,Environment 实际上也是委托 PropertySourcesPropertyResolver 完成 占位符的解析和类型转换。 类型转换又是委托 ConversionService 完成的。


关于 PropertyResolver 的更多讲解参考:Spring PropertyResolver 占位符解析(一)API 介绍


3、 refresh()


该方法是 Spring Bean 加载的核心,它是  ClassPathXmlApplicationContext 的父类 AbstractApplicationContext 的一个方法 , 顾名思义,用于刷新整个Spring 上下文信息,定义了整个 Spring 上下文加载的流程。


关于此方法的解读由于篇幅过长,所以会单独写一篇文章,欢迎阅读 Spring IoC之ApplicationContext中refresh过程


构造方法之paths


对于构造方法A的代码分析结束了,接下来顺带了解一下构造方法B,看看有何差别。


public ClassPathXmlApplicationContext(String[] paths, Class<?> clazz, @Nullable ApplicationContext parent) throws BeansException {
    super(parent);
    Assert.notNull(paths, "Path array must not be null");
    Assert.notNull(clazz, "Class argument must not be null");
    this.configResources = new Resource[paths.length];
    for(int i = 0; i < paths.length; ++i) {
        this.configResources[i] = new ClassPathResource(paths[i], clazz);
    }
    this.refresh();
}
复制代码


使用方式:


@Test
public void MyBean(){
    ApplicationContext context = new ClassPathXmlApplicationContext("/application_context.xml", UserDao.class);
    //System.setProperty("spring","application_context");
    //ApplicationContext context = new ClassPathXmlApplicationContext("/${spring}.xml", UserDao.class);    //调用失败
    System.out.println(context.getParent());
    System.out.println(context.getEnvironment());
    System.out.println(context.getClassLoader());
    User user = (User) context.getBean("user");
    System.out.println(user);
}
复制代码


执行结果为:


null
StandardEnvironment {activeProfiles=[], defaultProfiles=[default], propertySources=[PropertiesPropertySource {name='systemProperties'}, SystemEnvironmentPropertySource {name='systemEnvironment'}]}
sun.misc.Launcher$AppClassLoader@18b4aac2
User{name='hresh'}
复制代码


接着我们分析在代码实现方面这两个构造方法有何区别。

首先都有 super(parent)方法,关于此方法的调用是一致的。构造方法A中关于设置文件路径有自己的实现方法 setConfigLocations,而构造方法B中无此代码实现。具体差别可以看上文中的使用方法,构造方法B没法使用占位符设置配置文件路径。之后关于 refresh 方法中代码,主要区别在 AbstractXmlApplicationContext 中的 loadBeanDefinitions 方法。


protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
    //构造方法B调用
    Resource[] configResources = this.getConfigResources();
    if (configResources != null) {
        reader.loadBeanDefinitions(configResources);
    }
    //构造方法A使用
    String[] configLocations = this.getConfigLocations();
    if (configLocations != null) {
        reader.loadBeanDefinitions(configLocations);
    }
}
复制代码


总结


Spring 是一个轻量级的控制反转(IoC)和面向切面(AOP)的开源容器(框架)。学习使用 Spring 很长一段时间了,关于 Spring 中比较重要的 IoC 功能终于结合代码分析了一遍。相关涉及到的分支比较多,一篇文章无法讲述完,后续我会将其中比较重要的知识点再拿出来单独解析一下。


目录
相关文章
|
4月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
16天前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
38 2
|
1月前
|
XML 缓存 Java
搞透 IOC、Spring IOC ,看这篇就够了!
本文详细解析了Spring框架的核心内容——IOC(控制反转)及其依赖注入(DI)的实现原理,帮助读者理解如何通过IOC实现组件解耦,提高程序的灵活性和可维护性。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
|
3月前
|
XML Java 数据格式
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
Spring 第二节内容补充 关于Bean配置的更多内容和细节 万字详解!
246 18
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
|
3月前
|
XML Java 测试技术
spring复习01,IOC的思想和第一个spring程序helloWorld
Spring框架中IOC(控制反转)的思想和实现,通过一个简单的例子展示了如何通过IOC容器管理对象依赖,从而提高代码的灵活性和可维护性。
spring复习01,IOC的思想和第一个spring程序helloWorld
|
1月前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
39 0
|
2月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
144 9
|
2月前
|
存储 开发框架 Java
什么是Spring?什么是IOC?什么是DI?IOC和DI的关系? —— 零基础可无压力学习,带源码
文章详细介绍了Spring、IOC、DI的概念和关系,解释了控制反转(IOC)和依赖注入(DI)的原理,并提供了IOC的代码示例,阐述了Spring框架作为IOC容器的应用。
38 0
什么是Spring?什么是IOC?什么是DI?IOC和DI的关系? —— 零基础可无压力学习,带源码
|
3月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
46 4
|
2月前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
79 0
下一篇
DataWorks