Spring系列1-IOC

103 阅读8分钟

最近在对公司框架进行二次改造封装运用到了很多spring生命周期相关的东西,复习整理一下,顺便总结一下相关的使用。

SpringIOC

IOC介绍

IOC的重要性

Spring的IOC可以说是Spring最核心的功能,也是整个Spring体系的基石;基于IOC衍生出了许多强大的功能组件,即使AOP的成功也得益于IOC的容器管理,至于更上层的组件,Spring Data、Spring WEB、Spring TX、Spring JMS等这些都是在IOC容器的基础上开发。

IOC,主要的功能是用来管理应用的实例对象,包括对象生命周期管理、依赖管理、控制反转等。

spring-framework-架构图.png

以上是我从网上找到一个Spring结构图,可以体现出底层的Core Container就是整个Spring应用的基石。

Spring之所以如此成功也跟IOC的设计有关,目前来看大多数Java框架或者组件都可以交给IOC管理,让其能够进入融入到Spring体系中,让其统一管理。最开始的Spring就跟一个大房子一样,只要注册后,都可以进入Spring这个房子,在这个体系内组件可以相互使用,从整合角度来说它就像一个胶水框架,能够黏住需要实例化的Java组件,将生命周期和组件依赖等相关职责交给IOC管理。后面基于这个理念,又延伸出了SpringBoot,在具备Spring原有的强大包容性的基础上,对各个常用组件进行了二次封装,简化了大量组件的初始化配置,以及整合步骤,让其开发者能够更加方便的使用。

Spring使用之IOC

现在好多一来都直接使用SpringBoot依赖一引入,包自动生成点一下启动就可以启动一个Spring应用。

如以下这种:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

这种模式确实降低了学习成本,提高了易用性,但是也屏蔽了大量细节底层,对于初学者来说少了很多整合的弯路,也就是少了很多毒打,从而导致出现了某些疑难杂症都不知道从何下手,不便于研究其原理。

作为一个使用Spring从SSH到现在的SpringCloud的程序员,一路看着Spring高速发展,惊喜于Spring每次的迭代,从以前整合个SSH、SSM,因为框架版本问题导致适配各种问题加班熬夜历历在目,所以得感谢SpringBoot和现在的Cloud,为Java程序员入门大大降低了门槛。

好了,缅怀完了,接下来,正式从Spring的使用分析IOC体系和原理。

基于XML装配Bean

ClassPathXmlApplicationContext cpxApp = 
                new ClassPathXmlApplicationContext("classpath:spring.xml");
Object user = cpxApp.getBean("user");
System.out.println(user);

spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="user" class="com.example.demo.entity.User">

    </bean>
</beans>

最开始Spring ApplicationContext的使用方式,在SSH,SSM打天下的时代,这种应用特别多,提倡配置大于约定,因为那时候还没啥约定,生态不像现在这么成熟,所以bean都在xml中声明,依赖等也得通过配置属性注入。部署都时候结合配置Spring提供的 ContextLoaderListener配置在web.xml中自动创建spring应用,即可完成一个部署在tomcat里面的spring应用。

基于注解装配Bean

后来在注解流行后,又诞生以注解为主的ApplicationContext

@Configuration
public class SpringIOCDemo {

    @Bean
    public User user(){
        return new User();
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationApp = new AnnotationConfigApplicationContext(SpringIOCDemo.class);
        Object user = annotationApp.getBean("user");
        System.out.println(user);

    }
}

可以看到AnnotationConfigApplicationContext这种方式,主要依赖于Spring提供的相关注解,进行装载,如@Bean。这种方式更加清晰明了,不用写冗余的xml配置文件,因为xml文件,需要编写大量的重复标签,如:bean、property等,而且如果在IDE不智能的情况下bean注入出错,写错等问题时常发生,这也是为什么现在逐渐淘汰了xml配置的原因。

这也是目前Spring的主要应用装载方式,如SpringBoot我们可以看到@SpringBootApplication注解标识启动程序,也可以大致猜出了SpringBoot也是基于注解启动装载的,但是SpringBoot更加复杂点,有好几种,只能说AnnotationConfigApplicationContext是SpringBoot的默认ApplicationContext,Web应用通常不是它,但是也异曲同工。

SpringBoot的ApplicationContext

protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                switch(this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
                    break;
                case REACTIVE:
                    contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
                    break;
                default:
                    contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
                }
            } catch (ClassNotFoundException var3) {
                throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
            }
        }

        return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
    }

可以看到,虽然不是使用的AnnotationConfigApplicationContext,但是其它两种也是带Annotation开头的,都是获取注解,因此现在Spring应用注解使用为主流。

ApplicationContext

前面介绍了主流的两种ApplicationContext的使用,也反应了Spring几年的发展历程。可以看到ApplicationContext是Spring启动装载的入口也是核心,ApplicationContext就是SpringIOC的主要实现。

2022-08-20-12-11-27-image.png

可以看到ApplicationContext还有很多的实现,可以根据不同场景下选择相应的ApplicationContext创建Spring应用,常用的就是ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、AnnotationConfigApplicationContext这几种,反正我手动操作创建过的就这三种。

ApplicationContext体系结构

在分析体系结构前要先明确ApplicationContext是用来干嘛的,在Spring应用中承担了什么样的角色,从上文分析来看初始化一个启动一个Spring应用就是new一个xxxApplicationContext因此可以大致总结出,ApplicationContext就是Spring的核心,入口,也可以说这就是Spring框架的具体实现。

ApplicationContext就是SpringIOC,那么它是不是也就具备了Spring的能力,Spring的主要能力是管理bean也就是我们说的beanFactory,管理bean的依据是什么呢?这个就是具体的实现为准,比如ClassPathXmlApplicationContext就是以classpath下的xml作为spring容器初始化依据,AnnotationConfigApplicationContext就是以标识了spring容器注解的类作为依据。明白了这两点,再来看ApplicationContext的类图就非常清晰明了了。

2022-08-20-20-59-31-image.png

可以看到ApplicationContext是一个接口并且都继承了BeanFactory和ResourceLoader。按照接口的定义来看,要实现一个ApplicationContext需要提供BeanFactory的能力,也就是我们常用的getBean的相关方法,因为bean的创建不是凭空出现的,需要有依据,也就是我们说的配置,因此必须要指定入口,也就有了ResourceLoader接口。这时候再回过头看那么多的ApplicationContext就能够一下分析出其体系,它们只是按照相应配置加载方式不同提供了不同的实现而已。

ApplicationContext通常主要有两大子类实现,分别是AbstractRefreshableApplicationContextGenericApplicationContext

AbstractRefreshableApplicationContext

AbstractRefreshableApplicationContext是一偏向垂直抽象扩展ApplicationContext,它只支持ApplicationContext的功能,更加关注垂直扩展中,BeanDefinition的来源,提供了一个重要抽象方法loadBeanDefinitions用来加载BeanDefinition。适用于应用创建的时候就已经确定了所有bean,没有动态扩展。如ClassPathXmlApplicationContextFileSystemXmlApplicationContext这种ApplicationContext,在应用创建的时候就已经完成了bean的声明,更加关注容器的刷新与创建。

通常使用就是重写loadBeanDefinitions方法:

   ClassPathXmlApplicationContext cpxApp =  new ClassPathXmlApplicationContext("classpath:spring.xml");

   // ClassPathXmlApplicationContext#ClassPathXmlApplicationContext(java.lang.String[], boolean, org.springframework.context.ApplicationContext)
   public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
        super(parent);
       // 设置beanDefinition来源
        this.setConfigLocations(configLocations);
        if (refresh) {
            this.refresh();
        }

    }
   // AbstractXmlApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.xml.XmlBeanDefinitionReader)
    protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
        Resource[] configResources = this.getConfigResources();
        if (configResources != null) {
            reader.loadBeanDefinitions(configResources);
        }
        // 获取ClassPathXmlApplicationContext设置的xml
        String[] configLocations = this.getConfigLocations();
        if (configLocations != null) {
             // 读取xml,并将bena注册进beanFactory中
            reader.loadBeanDefinitions(configLocations);
        }

    }

GenericApplicationContext

AbstractRefreshableApplicationContext它相对于GenericApplicationContext最大的区别在于GenericApplicationContext实现了BeanDefinitionRegistry接口并且GenericApplicationContext是一个实现类,可以直接使用,提供了默认bean注册到Beanfactory。

从扩展性来看,我们要自己实现一个定制化的Applicationcontext,使用AbstractRefreshableApplicationContext必须要创建个实现类,来继承它,自己手动实现将BeanDefinition注册到beanFactory中。而GenericApplicationContext持有一个DefaultListableBeanFactory实例,实现了BeanDefinitionRegistry的能力,能够直接使用。

GenericApplicationContext ctx = new GenericApplicationContext();
// 使用任意的Reader
XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
xmlReader.loadBeanDefinitions(new ClassPathResource("spring.xml"));
// 多次加载beanDefinition
PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(ctx);
propReader.loadBeanDefinitions(new ClassPathResource("spring-ext.properties"));

 ctx.registerBean(User.class);
//调用Refresh方法
ctx.refresh();

可以看到上面的使用,直接通过组合的方式就能够直接创建一个Applicationcontext并且支持多次加载,支持动态注册bean。比较有代表性的就是SpringBoot的ServletWebServerApplicationContext就是继承该类实现的。

自己实现一个ApplicationContext

了解了上面的ApplicationContext我们完全可以定制化一些自己的ApplicationContext。

比如我们曾经有个很骚的操作,就是为了集中管控某些组件,需要将实例化这些组件的xml给放到类似于现在分布式配置中心的一个远程应用中集中管理,然后各个应用在启动的时候在远程配置中心中读取到xml进行Spring容器启动,这将相当于本地没有xml,xml需要从远程读取,然后启动Spring应用。这时候咋个办呢,很明显Spring并没有提供这种能力,那么我们在了解了整个体系后,可以很简单的实现这个功能,

ClassPathXmlApplicationContext和FileSystemXmlApplicationContext声明:

public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext
public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext

参考本地的两种xml实现,即可写出如下伪代码:

public class RemoteXmlApplicationContext extends AbstractXmlApplicationContext {
    public RemoteXmlApplicationContext() {
    }

    public RemoteXmlApplicationContext(ApplicationContext parent) {
        super(parent);
    }

    public RemoteXmlApplicationContext(String configLocationRemoteAddr) throws BeansException {
        // 读取远程xml到本地
        String[] configLocations = readXmlFileToLocation(configLocationRemoteAddr);
//        super(null);
        this.setConfigLocations(configLocations);
        this.refresh();
    }
}

这样就实现了一个通过读取远程xml来创建Spring应用的ApplicationContext,这个代码比较简单是因为AbstractXmlApplicationContext已经实现了xml解析装配等。如果要实现一个Spring不支持的存储类型的,需要自己重写loadBeanDefinitions方法将要实例化的bean初始化成BeanDefinition,什么是BeanDefinition就不介绍了,这是入门知识了。

可以通过数据库、txt、二进制等任意类型的存储方式来进行创建你想要的Spring应用,这都是基于ApplicationContext体系的。