Spring Boot 2.0 :深入分析Spring Boot原理

 2019-10-17 21:57  阅读(944)
文章分类:Spring boot

1.Spring IoC

Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。当初接触这个词汇时,表示一脸蒙比,什么控制?什么翻转?

  • 谁控制谁,控制什么:传统Java程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
  • 为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

我们知道IoC容器是用来存放实例化好的对象,并且管理对象之间的关系,IoC 容器想要管理各个业务对象以及它们之间的依赖关系,需要通过某种途径来记录和管理这些信息。 BeanDefinition对象就承担了这个责任:容器中的每一个 bean 都会有一个对应的 BeanDefinition 实例,该实例负责保存bean对象的所有必要信息,包括 bean 对象的 class 类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的 bean 实例。

有了Bean的抽象定义类,还需要一个工厂类来注册和管理Bean,Spring中提供了BeanDefinitionRegistry和 BeanFactory。BeanDefinitionRegistry 抽象出 bean 的注册逻辑,而 BeanFactory 则抽象出了 bean 的管理逻辑,而各个 BeanFactory 的实现类就具体承担了 bean 的注册以及管理工作。它们之间的关系就如下图:
20191017100291\_1.png

DefaultListableBeanFactory作为一个比较通用的 BeanFactory 实现,它同时也实现了 BeanDefinitionRegistry 接口,因此它就承担了 Bean 的注册管理工作。当然DefaultListableBeanFactory不仅仅实现了上面两个类。

下面通过一段简单的代码来模拟 BeanFactory 底层是如何工作的:

// 默认容器实现
    DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
    // 根据业务对象构造相应的BeanDefinition
    AbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true);
    // 将bean定义注册到容器中
    beanRegistry.registerBeanDefinition("beanName",definition);
    // 如果有多个bean,还可以指定各个bean之间的依赖关系
    // ........

    // 然后可以从容器中获取这个bean的实例
    // 注意:这里的beanRegistry其实实现了BeanFactory接口,所以可以强转,
    // 单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的
    BeanFactory container = (BeanFactory)beanRegistry;
    Business business = (Business)container.getBean("beanName");

这段代码仅为了说明 BeanFactory 底层的大致工作流程,实际情况会更加复杂,比如 bean 之间的依赖关系可能定义在外部配置文件(XML/Properties)中、也可能是注解方式。Spring IoC 容器的整个工作流程大致可以分为两个阶段:

  1. 容器启动时,会通过某种途径加载 ConfigurationMetaData。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如: BeanDefinitionReader,BeanDefinitionReader 会对加载的 ConfigurationMetaData进行解析和分析,并将分析后的信息组装为相应的 BeanDefinition,最后把这些保存了 bean 定义的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于 bean 对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。
  2. 经过第一阶段,所有 bean 定义都通过 BeanDefinition 的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。

BeanFactory 只是 Spring IoC 容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器: ApplicationContext,它构建在 BeanFactory 之上,属于更高级的容器,除了具有 BeanFactory 的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的 bean,在容器启动时全部完成初始化和依赖注入操作。

作用域
在默认情况下,Spring应用上下文中所有bean都是作为以单例(Singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例

Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

  • 单例(singleton):在整个应用中,只创建bean的一个实例。
  • 原型(prototype):每次注入或者通过spring应用上下文获取的时候,都会创建一个新的bean实例。
  • 会话(session):在web应用中,为每个会话创建一个bean实例。
  • 请求(request):在web应用中,为每个请求创建一个bean实例。

2.Sping AOP

AOP是aspect oriented programing的简称,意为面向切面编程。 spring aop使用了动态代理技术在运行期织入增强的代码,使用了两种代理机制,一种是基于jdk的动态代理,另一种是基于CGLib的动态代理。
详细请看深入理解代理模式设计模式:代理模式

3. JavaConfig与常见Annotation

JavaConfig

在最初,Spring使用XML配置文件的方式来描述bean的定义以及相互间的依赖关系,但随着Spring的发展,越来越多的人对这种方式表示不满,因为Spring项目的所有业务类均以bean的形式配置在XML文件中,造成了大量的XML文件,使项目变得复杂且难以管理。后来Spring及社区推出并持续完善了 JavaConfig子项目,它基于Java代码和Annotation注解来描述bean之间的依赖绑定关系。比如,下面是使用XML配置方式来描述bean的定义:

<bean id="bookService" class="cn.moondev.service.BookServiceImpl"></bean>

而基于JavaConfig的配置形式是这样的:

@Configuration
    public class MoonBookConfiguration {

        // 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中
        // 方法名默认成为该bean定义的id
        @Bean
        public BookService bookService() {
            return new BookServiceImpl();
        }
    }

如果两个bean之间有依赖关系的话,在XML配置中应该是这样:

<bean id="bookService" class="cn.moondev.service.BookServiceImpl">
        <property name="dependencyService" ref="dependencyService"/>
    </bean>
    <bean id="otherService" class="cn.moondev.service.OtherServiceImpl">
        <property name="dependencyService" ref="dependencyService"/>
    </bean>
    <bean id="dependencyService" class="DependencyServiceImpl"/>

而在JavaConfig中则是这样:

@Configuration
    public class MoonBookConfiguration {

        // 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可
        // 这里直接调用dependencyService()
        @Bean
        public BookService bookService() {
            return new BookServiceImpl(dependencyService());
        }

        @Bean
        public OtherService otherService() {
            return new OtherServiceImpl(dependencyService());
        }

        @Bean
        public DependencyService dependencyService() {
            return new DependencyServiceImpl();
        }
    }

有两个bean都依赖于dependencyService,也就是说当初始化bookService时会调用 dependencyService(),在初始化otherService时也会调用 dependencyService(),那么问题来了?这时候IOC容器中是有一个dependencyService实例还是两个?答案:一个。

@ComponentScan

@ComponentScan注解对应XML配置形式中的 元素,表示启用组件扫描,Spring会自动扫描所有通过注解配置的bean,然后将其注册到IOC容器中。我们可以通过 basePackages等属性来指定 @ComponentScan自动扫描的范围,如果不指定,默认从声明 @ComponentScan所在类的 package进行扫描。正因为如此,SpringBoot的启动类都默认在 src/main/java下。

@Import

@Import注解用于导入配置类,举个简单的例子:

@Configuration
    public class MoonBookConfiguration {
        @Bean
        public BookService bookService() {
            return new BookServiceImpl();
        }
    }

现在有另外一个配置类,比如: MoonUserConfiguration,这个配置类中有一个bean依赖于 MoonBookConfiguration中的bookService,如何将这两个bean组合在一起?借助 @Import即可:

@Configuration
    // 可以同时导入多个配置类,比如:@Import({A.class,B.class})
    @Import(MoonBookConfiguration.class)
    public class MoonUserConfiguration {
        @Bean
        public UserService userService(BookService bookService) {
            return new BookServiceImpl(bookService);
        }
    }

需要注意的是,在4.2之前, @Import注解只支持导入配置类,但是在4.2之后,它支持导入普通类,并将这个类作为一个bean的定义注册到IOC容器中。

@ConfigurationProperties与@EnableConfigurationProperties

当某些属性的值需要配置的时候,我们一般会在 application.properties文件中新建配置项,然后在bean中使用 @Value注解来获取配置的值,但是@Value的可扩展性较差,因为在每个用的地方都要写上@Value(“xxxxx”),如果xxxxx字符串改变,每个地方都要改变。因此Spring Boot提供了更优雅的实现方式,那就是 @ConfigurationProperties注解。

@Component
    // 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件
    @ConfigurationProperties("jdbc.mysql")
    // 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项
    pulic class JdbcConfig {
        public String url;
        public String username;
        public String password;
    }

    @Configuration
    public class HikariDataSourceConfiguration {

        @AutoWired
        public JdbcConfig config;

        @Bean
        public HikariDataSource dataSource() {
            HikariConfig hikariConfig = new HikariConfig();
            hikariConfig.setJdbcUrl(config.url);
            hikariConfig.setUsername(config.username);
            hikariConfig.setPassword(config.password);
            // 省略部分代码
            return new HikariDataSource(hikariConfig);
        }
    }

@EnableConfigurationProperties注解表示对 @ConfigurationProperties的内嵌支持,默认会将对应Properties Class作为bean注入的IOC容器中,即在相应的Properties类上不用加 @Component注解。

4. SpringFactoriesLoader详解

JVM提供了3种类加载器: BootstrapClassLoader、 ExtClassLoader、 AppClassLoader分别加载Java核心类库、扩展类库以及应用的类路径( CLASSPATH)下的类库。JVM通过双亲委派模型进行类的加载,我们也可以通过继承 java.lang.classloader实现自己的类加载器。

何为双亲委派模型?当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。

采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证Java 核心库的类型安全,比如,加载位于rt.jar包中的 java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。

但双亲委派模型并不能解决所有的类加载器问题,比如,Java 提供了很多服务提供者接口( ServiceProviderInterface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些SPI的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由BootstrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载的。BootstrapClassLoader是无法找到 SPI 的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。

线程上下文类加载器( ContextClassLoader)正好解决了这个问题。从名称上看,可能会误解为它是一种新的类加载器,实际上,它仅仅是Thread类的一个变量而已,可以通过 setContextClassLoader(ClassLoadercl)和 getContextClassLoader()来设置和获取该对象。如果不做任何的设置,Java应用的线程的上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。但在JDBC中,你可能会看到一种更直接的实现方式,比如,JDBC驱动管理 java.sql.Driver中的 loadInitialDrivers()方法中,你可以直接看到JDK是如何加载驱动的:

for (String aDriver : driversList) {
        try {
            // 直接使用AppClassLoader
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }

类加载器除了加载class外,还有一个非常重要功能,就是加载资源,它可以从jar包中读取任何资源文件,比如, ClassLoader.getResources(Stringname)方法就是用于读取jar包中的资源文件,其代码如下:

public Enumeration<URL> getResources(String name) throws IOException {
        Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2]; if (parent != null) { tmp[0] = parent.getResources(name); } else { tmp[0] = getBootstrapResources(name); } tmp[1] = findResources(name); return new CompoundEnumeration<>(tmp); }

是不是觉得有点眼熟,不错,它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到BootstrapClassLoader,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的jar包,就如同加载class一样,最后会扫描所有的jar包,找到符合条件的资源文件。

类加载器的 findResources(name)方法会遍历其负责加载的所有jar包,找到jar包中名称为name的资源文件,这里的资源可以是任何文件,甚至是.class文件,比如下面的示例,用于查找Array.class文件:

// 寻找Array.class文件
    public static void main(String[] args) throws Exception{
        // Array.class的完整路径
        String name = "java/sql/Array.class";
        Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            System.out.println(url.toString());
        }
    }

最终结果为$JAVA_HOME/jre/lib/rt.jar!/java/sql/Array.class
SpringFactoriesLoader中的应用:

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    // spring.factories文件的格式为:key=value1,value2,value3
    // 从所有的jar包中找到META-INF/spring.factories文件
    // 然后从文件中解析出key=factoryClass类名称的所有value值
    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        // 取得资源文件的URL
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
        // 遍历所有的URL
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            // 根据资源文件URL解析properties文件
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            String factoryClassNames = properties.getProperty(factoryClassName);
            // 组装数据,并返回
            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
        }
        return result;
    }

从 CLASSPATH下的每个Jar包中搜寻所有 META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。来简单看下 spring.factories文件的内容吧:

// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories
    // EnableAutoConfiguration后文会讲到,它用于开启Spring Boot自动配置功能
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
    org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
    org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\

执行 loadFactoryNames(EnableAutoConfiguration.class,classLoader)后,得到对应的一组 @Configuration类的名称字符串。我们就可以通过反射实例化这些类然后注入到IOC容器中,最后容器里就有了一系列标注了 @Configuration的JavaConfig形式的配置类。

这就是 SpringFactoriesLoader,它本质上属于Spring框架私有的一种扩展方案,类似于SPI,Spring Boot在Spring基础上的很多核心功能都是基于此。

5 . Spring容器的事件监听机制

过去,事件监听机制多用于图形界面编程,比如:点击按钮、在文本框输入内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件,而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。Java提供了实现事件监听机制的两个基础类:自定义事件类型扩展自 java.util.EventObject、事件的监听器扩展自 java.util.EventListener。来看一个简单的实例:简单的监控一个方法的耗时。

首先定义事件类型,通常的做法是扩展EventObject,随着事件的发生,相应的状态通常都封装在此类中:

public class MethodMonitorEvent extends EventObject {
        // 时间戳,用于记录方法开始执行的时间
        public long timestamp;

        public MethodMonitorEvent(Object source) {
            super(source);
        }
    }

事件发布之后,相应的监听器即可对该类型的事件进行处理,我们可以在方法开始执行之前发布一个begin事件,在方法执行结束之后发布一个end事件,相应地,事件监听器需要提供方法对这两种情况下接收到的事件进行处理:

// 1、定义事件监听接口
    public interface MethodMonitorEventListener extends EventListener {
        // 处理方法执行之前发布的事件
        public void onMethodBegin(MethodMonitorEvent event);
        // 处理方法结束时发布的事件
        public void onMethodEnd(MethodMonitorEvent event);
    }
    // 2、事件监听接口的实现:如何处理
    public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener {

        @Override
        public void onMethodBegin(MethodMonitorEvent event) {
            // 记录方法开始执行时的时间
            event.timestamp = System.currentTimeMillis();
        }

        @Override
        public void onMethodEnd(MethodMonitorEvent event) {
            // 计算方法耗时
            long duration = System.currentTimeMillis() - event.timestamp;
            System.out.println("耗时:" + duration);
        }
    }

事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收MethodMonitorEvent参数,说明这个监听器类只负责监听器对应的事件并进行处理。有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器:

public class MethodMonitorEventPublisher   {

        private List<MethodMonitorEventListener> listeners = new ArrayList<MethodMonitorEventListener>();

        public void methodMonitor() {
            MethodMonitorEvent eventObject = new MethodMonitorEvent(this);
            publishEvent("begin", eventObject);
            // 模拟方法执行:休眠5秒钟
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            publishEvent("end", eventObject);

        }

        private void publishEvent(String status, MethodMonitorEvent event) {
            // 避免在事件处理期间,监听器被移除,这里为了安全做一个复制操作
            for (MethodMonitorEventListener listener : listeners) {
                if ("begin".equals(status)) {
                    listener.onMethodBegin(event);
                } else {
                    listener.onMethodEnd(event);
                }
            }
        }

        public static void main(String[] args) {
            MethodMonitorEventPublisher publisher = new MethodMonitorEventPublisher();
            publisher.addEventListener(new AbstractMethodMonitorEventListener());
            publisher.methodMonitor();
        }

        public void addEventListener(MethodMonitorEventListener listener) {
            listeners.add(listener);
        }

    }

对于事件发布者(事件源)通常需要关注两点:

  • 在合适的时机发布事件。此例中的methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布MethodMonitorEvent事件,每个时间点发布的事件都会传给相应的监听器进行处理。在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。
  • 事件监听器的管理。publisher类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。如果这里没有提供remove方法,那么注册的监听器示例将一直被MethodMonitorEventPublisher引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。

Spring容器内的事件监听机制

Spring的ApplicationContext容器内部中的所有事件类型均继承自 org.springframework.context.AppliationEvent,容器中的所有监听器都实现 org.springframework.context.ApplicationListener接口,并且以bean的形式注册在容器中。一旦在容器内发布ApplicationEvent及其子类型的事件,注册到容器的ApplicationListener就会对这些事件进行处理。
20191017100291\_2.png
容器内部使用ApplicationListener作为事件监听器接口定义,它继承自EventListener。ApplicationContext容器在启动时,会自动识别并加载EventListener类型的bean,一旦容器内有事件发布,将通知这些注册到容器的EventListener。

ApplicationContext接口继承了ApplicationEventPublisher接口,该接口提供了 voidpublishEvent(ApplicationEventevent)方法定义,不难看出,ApplicationContext容器担当的就是事件发布者的角色。

6. 自动配置原理

任何一个Spring Boot项目,都会用到如下的启动类

@SpringBootApplication
    public class SpringboootdemoApplication {

        public static void main(String[] args) {

            SpringApplication.run(SpringboootdemoApplication.class, args);
        }
    }

主要的两行代码是@SpringBootApplication和SpringApplication.run,下面从这两个点来分析Spring boot启动过程。

@SpringBootApplication注解的源码点击去查看,发现在他内部主要使用了下面三个注解,在上一节简单的提到过。下面逐个分析。

@Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(
        excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
    )
    public @interface SpringBootApplication {
        ...
    }
  • @Configuration(@SpringBootConfiguration内部应用了@Configuration)
  • @EnableAutoConfiguration
  • @ComponentScan
    当然这三个注解也可以替代@SpringBootApplication注解,只是不方便而已。

@Configuration:用@Configuration注解该类,等价 与XML中配置beans;用@Bean标注方法等价于XML中配置bean。任何一个标注了@Configuration的Java类定义都是一个JavaConfig配置类。

@ComponentScan:功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。所以SpringBoot的启动类最好是放在root package下,因为默认不指定basePackages。

@EnableAutoConfiguration
该接口的定义为:

@Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import({AutoConfigurationImportSelector.class})
    public @interface EnableAutoConfiguration {
        String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

        Class<?>[] exclude() default {};

        String[] excludeName() default {};
    }

@EnableAutoConfiguration作为一个复合annotation,其中最关键的要属@Import(AutoConfigurationImportSelector.class),借助AutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助springboot应用将所有符合条件的@Configuration配置都加载到当前springboot创建并使用的ioc容器,就跟一只八爪鱼一样。

AutoConfigurationImportSelector借助spring框架原有的一个工具类SpringFactoriesLoader的支持, SpringFactoriesLoader主要功能是从指定的配置文件META-INF/spring.factories加载配置,spring.factories是一个典型的Java properties文件,配置的格式为key=value形式,只不过key和value都是Java类型的完整类型

@EnableAutoConfiguration的自动配置就是从classpath中搜寻所有META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的配置项通过反射实例化为对应的标注了@Configuration的JavaConfig形式的ioc容器配置类,然后汇总为一个并加载到ioc容器。(挑战)

7.SpringApplication启动流程

首先进入run()方法

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
        return run(new Class<?>[] { primarySource }, args);
    }

    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return new SpringApplication(primarySources).run(args);
    }

run方法中去创建了一个SpringApplication实例,并且又调用了run(String… args)方法,下面分为两步分分析,一是创建SpringApplication实例的过程,也叫初始化过程。二是启动过程也就是run(String… args)做了哪些事情。

初始化/构造方法

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
            this.resourceLoader = resourceLoader; //1
            Assert.notNull(primarySources, "PrimarySources must not be null");
            this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));//2
            this.webApplicationType = deduceWebApplicationType();//3
            setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));//4
            setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//5
            this.mainApplicationClass = deduceMainApplicationClass();//6
        }
  1. 在源码中调用此方法第一个参数传入的null,把null赋值给了成员变量resourceLoader。

  2. 首先primarySources是一个Set类型的成员变量,它存放的是Class类型的对象,我们在启动时把SpringboootdemoApplication.class传入

  3. WebApplicationType是一个枚举类型,它有三种类型

    • NONE:应用程序不应该作为Web应用程序运行,不应该启动嵌入式Web服务器。
    • SERVLET:应用程序应作为基于servlet的Web应用程序运行,并应启动嵌入式servlet Web服务器。
    • REACTIVE:应用程序应作为响应式Web应用程序运行,并应启动嵌入式响应式Web服务器。

    deduceWebApplicationType就是根据当前的环境判断处,并返回webApp的类型

  4. 通过反射实例化了ApplicationContextInitializer,并且把对象添加到initializers中,其中initializers是List结构,它保存了ApplicationContextInitializer的实例。

  5. 和4中同理,保存程序事件监听器ApplicationListener

  6. 找出启动类,设置到 mainApplicationClass 中

初始化流程中最重要的就是通过SpringFactoriesLoader找到 spring.factories文件中配置的 ApplicationContextInitializer和 ApplicationListener两个接口的实现类名称,以便后期构造相应的实例。 ApplicationContextInitializer的主要目的是在 ConfigurableApplicationContext做refresh之前,对ConfigurableApplicationContext实例做进一步的设置或处理。ConfigurableApplicationContext继承自ApplicationContext,其主要提供了对ApplicationContext进行设置的能力。

Spring Boot提供两种方式来添加自定义监听器:

  • 通过 SpringApplication.addListeners()或者 SpringApplication.setListeners()两个方法来添加一个或者多个自定义监听器

  • 既然SpringApplication的初始化流程中已经从 spring.factories中获取到 ApplicationListener的实现类,那么我们直接在自己的jar包的 META-INF/spring.factories文件中新增配置即可:

    org.springframework.context.ApplicationListener=
    cn.moondev.listeners.xxxxListener\

启动流程

Spring Boot应用的整个启动流程都封装在SpringApplication.run方法中,其整个流程真的是太长太长了,但本质上就是在Spring容器启动的基础上做了大量的扩展,按照这个思路来看看源码:

public ConfigurableApplicationContext run(String... args) {
            //构造一个任务执行观察器
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();// 开始执行,记录开始时间
            ConfigurableApplicationContext context = null;
            Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
            configureHeadlessProperty();
            //获取SpringApplicationRunListeners,内部只有一个EventPublishingRunListener 1
            SpringApplicationRunListeners listeners = getRunListeners(args);
            listeners.starting();
            try {
                //创建一个 DefaultApplicationArguments 对象,它持有 args 参数,就是 main 函数传进来的参数调用 prepareEnvironment 方法。 2
                ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
                ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
                configureIgnoreBeanInfo(environment);
                //3
                Banner printedBanner = printBanner(environment);
                //创建文ApplicationContext 4
                context = createApplicationContext();
                exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                        new Class[] { ConfigurableApplicationContext.class }, context);
                //5
                prepareContext(context, environment, listeners, applicationArguments,printedBanner);
                //6
                refreshContext(context);
                afterRefresh(context, applicationArguments);
                stopWatch.stop();
                if (this.logStartupInfo) {
                    new StartupInfoLogger(this.mainApplicationClass)
                            .logStarted(getApplicationLog(), stopWatch);
                }
                listeners.started(context);
                callRunners(context, applicationArguments);
            }
            catch (Throwable ex) {
                handleRunFailure(context, ex, exceptionReporters, listeners);
                throw new IllegalStateException(ex);
            }

            try {
                listeners.running(context);
            }
            catch (Throwable ex) {
                handleRunFailure(context, ex, exceptionReporters, null);
                throw new IllegalStateException(ex);
            }
            return context;
        }

下面根据代码的编号进行分析:
1. 通过SpringFactoriesLoader查找并加载所有的 SpringApplicationRunListeners,通过调用starting()方法通知所有的SpringApplicationRunListener:应用开始启动了。SpringApplicationRunListener其本质上就是一个事件发布者,它在SpringBoot应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。前面的初始化流程中,SpringApplication加载了一系列ApplicationListener。发布事件的代码已经在SpringApplicationRunListeners中实现了。

看下SpringApplicationRunListener的源码:

public interface SpringApplicationRunListener {

        //首次启动run方法时立即调用。 可用于早期的初始化。
        void starting();
        //一旦准备好环境就调用,但是在创建ApplicationContext之前调用
        void environmentPrepared(ConfigurableEnvironment environment);
        //一旦创建并准备了ApplicationContext,但在加载源之前调用
        void contextPrepared(ConfigurableApplicationContext context);
        //在应用程序上下文加载之后但在刷新之前调用
        void contextLoaded(ConfigurableApplicationContext context);
        //上下文已刷新且应用程序已启动但尚未调用CommandLineRunner CommandLineRunners和ApplicationRunner ApplicationRunners。
        //2.0之后添加的
        void started(ConfigurableApplicationContext context);
        //在run方法完成之前立即调用,即刷新应用程序上下文并调用所有CommandLineRunners和ApplicationRunner后调用。
        void running(ConfigurableApplicationContext context);
        //在运行应用程序时发生故障时调用。
        void failed(ConfigurableApplicationContext context, Throwable exception);
    }

SpringApplicationRunListener只有一个实现类: EventPublishingRunListener。1处的代码只会返回一个SpringApplicationRunListeners ,注意后面多了一个s字母,看下源码就会发现该类就是包含了一个SpringApplicationRunListener的List。操作SpringApplicationRunListeners ,在内部会遍历每一个SpringApplicationRunListener。至于为什么这么设计,笔者看来是为了易于扩展吧。。。下图是启动过程中Debug截图:
20191017100291\_3.png

1处的代码其实是遍历每个SpringApplicationRunListener,调用每一个SpringApplicationRunListener的实现类的starting方法,如下:

@Override
        public void starting() {
            //发布一个ApplicationStartedEvent
            this.initialMulticaster.multicastEvent(
                    new ApplicationStartingEvent(this.application, this.args));
        }

2. 创建并配置当前应用将要使用的 Environment,Environment用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当Environment准备好后,在整个应用的任何时候,都可以从Environment中获取资源。

总结起来,②处的两句代码,主要完成以下几件事:

  • 判断Environment是否存在,不存在就创建(如果是web项目就创建 StandardServletEnvironment,否则创建 StandardEnvironment)
  • 配置Environment:配置profile以及properties
  • 调用SpringApplicationRunListener的 environmentPrepared()方法,通知事件监听者:应用的Environment已经准备好

3. 打印Banner图案
4. 根据不同的ApplicationType创建不同的Context,具体的类型回顾初始化中App类型的介绍
**5.**初始化ApplicationContext,主要完成以下工作:

  • 将准备好的Environment设置给ApplicationContext
  • 遍历调用所有的ApplicationContextInitializer的 initialize()方法来对已经创建好的ApplicationContext进行进一步的处理
  • 调用SpringApplicationRunListener的 contextPrepared()方法,通知所有的监听者:ApplicationContext已经准备完毕
  • 将所有的bean加载到容器中
  • 调用SpringApplicationRunListener的 contextLoaded()方法,通知所有的监听者:ApplicationContext已经装载完毕

6. refresh完成配置类的解析、各种BeanFactoryPostProcessor和BeanPostProcessor的注册、国际化配置的初始化、web内置容器的构造等等。

写在最后

Spring boot的启动过程大致说完了,但是还有很多很多细节没有提到,先就这样吧!


来源:[]()

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> Spring Boot 2.0 :深入分析Spring Boot原理

相关推荐