SpringBoot 启动分析(五) — 上下文的刷新过程

1. SpringApplication.refreshContext

首先来看 SpringApplication 里刷新上下文的逻辑:

private void refreshContext(ConfigurableApplicationContext context) {
    refresh(context);
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
        } catch (AccessControlException ex) {
            // Not allowed in some environments.
        }
    }
}

protected void refresh(ApplicationContext applicationContext) {
    Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
    ((AbstractApplicationContext) applicationContext).refresh();
}

刷新的逻辑是在 AbstractApplicationContext.refresh 方法完成的,刷新完后注册了 JVM 的关闭回调钩子。

2. AbstractApplicationContext.refresh

继续阅读

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 方法

继续阅读

Motan RPC 框架分析

一、框架简介

Motan 是新浪微博开源的一套高性能、易于使用的分布式远程服务调用(RPC)框架。

Motan 的核心模块交互关系如下:

二、核心模块介绍

2.0 SPI 机制

SPI 机制支持 JDK 的 ServiceProvider 机制并进行了扩展,接口的实现放在 META-INF/services/ 目录下以接口的完全类名命名的文件里,每个实现的完全类名占一行。

每个实现类可以加上 SpiMeta(name="implName") 来指定名称,ExtensionLoader 可以通过接口类型和命名从多个实现中找到指定实现。

继续阅读

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。

继续阅读

Consul 与 K8S 滚动部署

不停机发布讨论

近期公司的运维同事对一些系统尝试利用 K8S 的滚动发布机制不停机发布系统,应用和系统的状况是:POD 里的应用实例是注册在 Consul 上,Consul 的健康检查间隔是 10 秒;它们配置的 K8S 滚动发布机制直接把跑应用的 POD 终止了。

这样出现了:应用实例被终止后到 Consul 下次健康检查之前,Consul 认为应用实例是还存活的,把请求分发给了已经被停止的实例,这些请求当然是失败的。有的系统因为下游不支持重试,之前出现应用重试导致出现异常数据,就把 Ribbon、Feign、Hystrix 等的重试机制禁用了。而没有了重试机制请求就直接失败了,这样用户就会感知到系统发布。

大家开会讨论怎么才能让系统发布不会影响请求响应:

  1. 另一个部门的同事说他们就是所有接口都支持重试,所以允许对服务的某个实例请求失败可以重试到另一个实例,这样基本不会影响请求处理。让所有接口支持重试是需要上下游系统协调的,而且需要比较长的时间来实现。还有个问题是,不是所有接口都能重试的,举个行业特定的例子:信贷审批一般都会去查借款人的人行征信报告,多查一次可能导致借款人几个月都没法借款,设想一个报告查询的请求发出去了、在响应还没落地之前,应用实例就被终止了,这肯定是没法接受的。所以应用实例需要在被终止之前得到一个机会—-可以优雅停机的机会,它需要一个停机的通知。对于前面的问题,就是 K8S 要在停机前通知到 POD 里的实例,等通知返回后再终止 POD。

  2. 这就带来这个小插曲,我提出 K8S 在停止 POD 之前给 POD 里的应用发个通知的时候,运维同事说 Consul 和 K8S 是不同的系统,没法做到,他们说的非常肯定。。。

K8S POD 生命周期管理

对于一个问题,自己解决不了 跟 问题无解 是有区别。

在网上搜索一番后找到了 POD 生命周期钩子

继续阅读