SpringBoot 启动分析(四) — 注解驱动的 Bean 定义加载

1. 一个 Spring 加载类的问题

先抛出个问题:SpringBoot 允许通过注解根据某个类是否存在来决定配置,如

@Bean
@ConditionalOnClass(value = HikariDataSource.class)
public DataSource hikariDataSource() {
    return new HikariDataSource();
}

@Bean
@ConditionalOnClass(value = BasicDataSource.class)
public DataSource basicDataSource() {
    return new BasicDataSource();
}

我们也知道 ClassLoader 加载一个类时,如果这个类或这个类依赖的类找不到则会抛出 ClassNotFoundException

Spring 是如何实现这样的条件加载而不会抛出 ClassNotFoundException 异常?

继续阅读

SpringBoot 启动分析(三) — Environment 的初始化流程

1. Environment 的初始化流程

ConfigFileApplicationListener 收到 ApplicationEnvironmentPreparedEvent 事件后通过 SPI 加载所有的 EnvironmentPostProcessor 实现,触发其 postProcessEnviroment 方法。

SpringApplication.run() ->
SpringFactoriesLoader.loadFactories(ApplicationListener) ->
SpringApplication.prepareEnviroment() -> EventPublishingRunListener.enviromentPrepared(ApplicationEnviromentPraparedEvent) ->
SimpleApplicationEventMulticaster.multicastEvent() ->
ConfigFileApplicationListener.onApplicationOnEnviromentPreparedEvent() ->
EnviromentPostProcessor.postProcessEnviroment()

比较重要的 EnviromentPostProcessor 实现是 HostInfoEnvironmentPostProcessorConfigFileApplicationListener

2. HostInfoEnvironmentPostProcessor.postProcessEnviroment

获取本机的 主机名和IP地址,封装在 PropertySource 添加到 environment 里。

3. ConfigFileApplicationListener.postProcessEnviroment

ConfigFileApplicationListener 自身也实现了 EnvironmentPostProcessor,通过内部类 Loader 去加载配置文件,其主要流程如下:

  1. 从 Environment 中获取 active 和 include 的 profile 集合。进行迭代:
  2. 获取所有的搜索路径,进行迭代,默认的搜索路径是 classpath:/,classpath:/config/,file:./,file:./config/
  3. 如果某个搜索路径不以 / 结尾的则认为是一个文件,直接加载,否则,找出所有的搜索文件名 name 进行迭代搜索,默认的搜索文件名是 “application”。
  4. 通过 PropertySourcesLoader 找出支持的所有配置文件后缀进行迭代。
  5. 最终得到 location + name + "-" + profile + "." + ext 组成的一个具体的完整路径,通过 PropertiesLoader.load 方法加载该路径指向的配置文件。
  6. PropertiesLoader.load 内部又根据配置文件的后缀用不同的 PropertySourceLoader 去加载得到一个 PropertySource
  7. 对于解析得到的 PropertySource,找出里面激活的 profile,添加到 proflie 集合里进行迭代。
  8. 继续迭代下一个 profile 。

继续阅读

SpringBoot 启动分析(一)

SpringBoot 启动分析 序列文章基于 spring-boot-starter-parent 1.5.19.RELEASE 。

1. 启动一个 SpringBoot 应用

启动一个 SpringBoot 应用只需要下面几行代码即可:

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

查看 SpringApplication.run 方法时会来到:

public static ConfigurableApplicationContext run(Object source, String... args) {
    return run(new Object[] { source }, args);
}

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
    return new SpringApplication(sources).run(args);
}

可以看到 SpringBoot 的魔法就是那么简单:创建一个 SpringApplication,执行其 run 方法
就像 “打开冰箱门、把大象塞进冰箱、关上冰箱门” 那么简单、有力。

当然,要了解原理是不能只看高层抽象的。

2. SPI 机制 SpringFactoriesLoader

SpringFactoriesLoader 是 Spring 提供的 SPI 实现机制,从类路径下的 META-INF/spring.factories 文件里加载指定接口的所有实现。以接口的完整类名作为 key,实现类的完整名字作为值,多个实现类用 , 分隔。

下面是 spring-boot-autoconfigure 包下 META-INF/spring.factories 文件里的 一小部分:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\

SpringFactoriesLoader 内部通过 ClassLoader.getResources 方法来加载类路径下的文件。

继续阅读

SpringBoot 启动分析(二)–启动主流程

1. initialize 方法

SpringApplication 的构造函数只调用了 initialize 方法:

private void initialize(Object[] sources) {
    if (sources != null && sources.length > 0) {
        // 把应用指定的配置类加入配置扫描的启动来源
        this.sources.addAll(Arrays.asList(sources));
    }

    // 判断应用是否是 web 应用,主要用于决定采用哪种具体的上下文实现
    this.webEnvironment = deduceWebEnvironment();

    // 利用定制的 SPI 实现 SpringFactoriesLoader 加载 ApplicationContextInitializer 的所有实现并设置到 initializers 属性
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

    // 利用定制的 SPI 实现 SpringFactoriesLoader 加载 ApplicationListener 的所有实现并设置到 listeners 属性
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

    // 找出启动类:线程栈上 main 方法所处的类
    this.mainApplicationClass = deduceMainApplicationClass();
}

该方法的逻辑主要如下:

  1. 把启动类加入 sources 属性。
  2. 判断是否是 web 应用并设置到 webEnvironment 属性。
  3. 加载所有的 ApplicationContextInitializer 并设置到 initializers 属性。
  4. 加载所有的 ApplicationListener 并设置到 listeners 属性。
  5. 找出 main 类设置到 mainApplicationClass。

2. run 方法

继续阅读

SpringBoot druid 踩坑笔记

这是一个同事碰到的案例。

现象

SpringBoot 应用卡死、无反应。

处理过程

1、 导出线程栈,发现 Tomcat 处理线程都阻塞在获取连接上,从栈上看连接池使用的是 druid。

2、 对照 druid 源码,发现线程一直被阻塞是因为没有设置获取连接的超时时间。而从配置来看是有设置的。被阻塞的线程栈如下:

"http-nio-8006-exec-200" #7057 daemon prio=5 os_prio=0 tid=0x00007fc82c0a3800 nid=0x1b99 waiting on condition [0x00007fc7c9a57000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000000c2923bd8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
    at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:1444)
    at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1088)
    at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:953)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4544)
    at com.alibaba.druid.filter.stat.StatFilter.dataSource_getConnection(StatFilter.java:661)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4540)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:931)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:923)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:100)

3、 通过内存 dump 发现,druid 连接池除 连接串、用户名、密码等几个属性之外的属性都是默认值。
此时连接池里总共有8个连接,都是空闲的,却没有线程能获取到连接,都在阻塞、没有被唤醒。网上查了下,应该是 druid 的bug。

继续阅读