分类 JavaWeb 下的文章

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

SpringBoot 启动流程

撰写本文时,源码参考上游最新版的 RELEASE 版 2.3.4.RELEASE, 那就直接从官方例子启动类开始挖啦。

1) SpringBoot 启动源码

@SpringBootApplication
public class Application {

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

@SpringBootApplication 注解我们暂时不提,下文中会重新讲到这个注解, SpringApplication.run 即 SpringBoot 启动的入口:

public class SpringApplication {
    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 方法向下走, 第一个 run 方法中用数组包装了一下启动类 Class, 第二个 run 方法中一共干了两件事儿, 不妨拆开来看:

  1. new SpringApplication(primarySources)
    创建 SpringApplication 实例的过程即 SpringBoot 初始化的过程, 在本文 1.1 会讲到
  2. .run(args)
    run 方法即 SpringBoot 真正的启动过程, 在本文 1.2 中讲到

1.1) SpringApplication

我们来看一下 new SpringApplication(primarySources) 的源码:

public class SpringApplication {
    public SpringApplication(Class<?>... primarySources) {
        this(null, primarySources);
    }
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }
}

清理掉非重要代码后, 大概做了以下几件事儿:

  1. this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    初始化 this.primarySource 主要资源类集合, 放入启动类, 暂且不提稍微眼熟一下
  2. this.webApplicationType = WebApplicationType.deduceFromClasspath();
    初始化 this.webApplicationType, 判断我们的启动程序类型, 参考 1.1.1
  3. setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    初始化 this.initializers 初始化器集合, 从 spring.factories 获取, 参考 1.1.2
  4. setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    初始化 this.listeners 监听器集合, 从 spring.factories 获取, 参考 1.1.3
  5. this.mainApplicationClass = deduceMainApplicationClass();
    初始化 this.mainApplicationClass 主应用 Class, 通过获取堆栈信息, 取调用链中最近的一个主函数类, 参考 1.1.4

1.1.1 SpringApplication.webApplicationType

初始化 this.webApplicationType, 判断我们的启动程序类型

this.webApplicationType = WebApplicationType.deduceFromClasspath();

public enum WebApplicationType {
    NONE,
    SERVLET,
    REACTIVE;
    private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
            "org.springframework.web.context.ConfigurableWebApplicationContext" };
    private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
    private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
    private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

    static WebApplicationType deduceFromClasspath() {
        if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
            return WebApplicationType.REACTIVE;
        }
        for (String className : SERVLET_INDICATOR_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return WebApplicationType.NONE;
            }
        }
        return WebApplicationType.SERVLET;
    }
}

这段源码很简单:

  1. 如程序依赖 SpringWebflux 但不依赖 SpringMVC 和 JERSEY, 即返回 REACTIVE 响应式程序
  2. 如程序不依赖 Servlet, 即返回 NONE 非 WEB 程序
  3. 否则即返回 SERVLET 程序

一般我们的项目都为 SERVLET 程序

1.1.2) SpringApplication.initializers

初始化 this.initializers 初始化器集合, 从 spring.factories 获取

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

先来看 getSpringFactoriesInstances 方法:

public class SpringApplication {
    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
        return getSpringFactoriesInstances(type, new Class<?>[] {});
    }
    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = getClassLoader();
        // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }
}
  1. ClassLoader classLoader = getClassLoader();
    获取类加载器, 不太重要这里不提

  2. Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    传入 ApplicationContextInitializer.class, 从 spring.factories 获取初始化器集合, 具体流程如下

     public final class SpringFactoriesLoader {
         public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
             String factoryTypeName = factoryType.getName();
             return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
         }
     }
    • 核心方法为 loadSpringFactories 返回 Map 类型集合, 然后从中获取 key 为 ApplicationContextInitializer.class 的初始化器类全路径集合

      public final class SpringFactoriesLoader {
         public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
      
         private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
             MultiValueMap<String, String> result = cache.get(classLoader);
             if (result != null) {
                 return result;
             }
      
             try {
                 Enumeration<URL> urls = (classLoader != null ?
                         classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                         ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
                 result = new LinkedMultiValueMap<>();
                 while (urls.hasMoreElements()) {
                     URL url = urls.nextElement();
                     UrlResource resource = new UrlResource(url);
                     Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                     for (Map.Entry<?, ?> entry : properties.entrySet()) {
                         String factoryTypeName = ((String) entry.getKey()).trim();
                         for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                             result.add(factoryTypeName, factoryImplementationName.trim());
                         }
                     }
                 }
                 cache.put(classLoader, result);
                 return result;
             }
             catch (IOException ex) {
                 throw new IllegalArgumentException("Unable to load factories from location [" +
                         FACTORIES_RESOURCE_LOCATION + "]", ex);
             }
         }
      }
    • 代码看着多, 但做的事儿很少, 大致如下:

      1. 尝试从缓存直接拿
      2. 拿到包含所有 spring.factories 路径的 urls
      3. 遍历每个 spring.factories 文件, 从中获取 key 为 ApplicationContextInitializer.class 的数据如下
        org.springframework.context.ApplicationContextInitializer=\
        org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
        org.springframework.boot.context.ContextIdApplicationContextInitializer,\
        org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
        org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
        org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
      4. 将 value split 一下与 key 一起放入 Map 集合中
      5. 缓存至内存让后续不再重复产生磁盘 IO
      6. 返回从 spring.factories 中取得的初始化器集合
  3. List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    创建初始化器实例, 上一步只是将初始化器类的全路径取得, 我们还需要反射创建一下实例:

     public final class SpringFactoriesLoader {
         private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
                 ClassLoader classLoader, Object[] args, Set<String> names) {
             List<T> instances = new ArrayList<>(names.size());
             for (String name : names) {
                 try {
                     Class<?> instanceClass = ClassUtils.forName(name, classLoader);
                     Assert.isAssignable(type, instanceClass);
                     Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
                     T instance = (T) BeanUtils.instantiateClass(constructor, args);
                     instances.add(instance);
                 }
                 catch (Throwable ex) {
                     throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
                 }
             }
             return instances;
         }
     }

    源码也很简单, 只是反射一下创建实例, 放进集合返回

  4. AnnotationAwareOrderComparator.sort(instances);
    顾名思义, 对集合排序, 若初始化器上有 @Order 注解则优先按照 order 数值排序, 从小到大, 这里不细讲了意义不大

最后调用 setInitializers 赋值到 initializers 中, 这里不贴代码了

初始化器部分就讲完啦, 总结:

  1. 从 spring.factories 中获取初始化器列表
  2. 创建初始化器实例
  3. 对初始化器通过 @Order 注解排序
  4. 赋值到 SpringApplication.initializers

1.1.3) SpringApplication.listeners

初始化 this.listeners 监听器集合, 从 spring.factories 获取
监听器的初始化和初始化器几乎一样, 可参考上一节 1.1.2

  1. 从 spring.factories 中获取初始化器列表
  2. 创建监听器实例
  3. 对监听器通过 @Order 注解排序
  4. 赋值到 SpringApplication.listeners

1.1.4) SpringApplication.mainApplicationClass

this.mainApplicationClass = deduceMainApplicationClass();

初始化 this.mainApplicationClass 主应用 Class, 通过获取堆栈信息, 取调用链中最近的一个主函数类
源码是一个很经典的获取方法调用链的例子:

public class SpringApplication {
    private Class<?> deduceMainApplicationClass() {
        try {
            StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
            for (StackTraceElement stackTraceElement : stackTrace) {
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        }
        catch (ClassNotFoundException ex) {
            // Swallow and continue
        }
        return null;
    }
}

看代码应该能看懂, 看不懂走一下 debug 就明白啦这里不讲了

1.2) SpringApplication.run

我们来康一哈源码:

public class SpringApplication {
    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            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. StopWatch stopWatch = new StopWatch(); stopWatch.start();
    开头的 StopWatch 相关的代码, 作用主要是记录程序启动的时间, 然后在程序基本启动完成后输出一下启动程序的耗时, 具体参考 21,23 行, 太简单这里不细讲
  2. ConfigurableApplicationContext context = null;
    定义了一下 Spring 的 ApplicationContext, 未赋值
  3. Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    初始化异常报告器集合, 用于存放从 spring.factories 中初始化的的异常报告器, 下面还会提到
  4. configureHeadlessProperty();
    设置系统参数 java.awt.headless 用于模拟各种服务器缺少的输入输出设备, 这里也不细讲, 可以百度 java headless
  5. SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting();
    从 spring.factories 初始化 SpringApplicationRunListeners, 它相当于 SpringBoot 监听器模型中, 广播器的一个包装工具类, 这里 start 了所有支持 ApplicationStartingEvent 事件的监听器, 参见 1.2.1
  6. ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    将命令行传参包装了一下, 方便取参

1.2.1) listeners.starting

先来看看 getRunListeners 这个方法:

public class SpringApplication {
    private SpringApplicationRunListeners getRunListeners(String[] args) {
        Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
        return new SpringApplicationRunListeners(logger,
                getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
    }
}

还是 getSpringFactoriesInstances 这个方法, 这里不重复提了, 从 spring.factories 读取 key 为 SpringApplicationRunListeners 的对象做初始化
, 在 spring-boot.jarspring-boot-autoconfigure.jar 中的 spring.factories 中能找到的类只有: org.springframework.boot.context.event.EventPublishingRunListener

class SpringApplicationRunListeners {

    private final Log log;

    private final List<SpringApplicationRunListener> listeners;

    SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
        this.log = log;
        this.listeners = new ArrayList<>(listeners);
    }
}

初始化后丢进成员变量, 啥事儿也没做, 那我们接着看看 listener.starting(); 吧, 这个才是本节的核心方法:

class SpringApplicationRunListeners {
    void starting() {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.starting();
        }
    }
}

上文嗦到 listeners 中只有一个 EventPublishingRunListener, 它是监听器模型中, 广播器的一个包装工具类, 可以优雅的广播相应的事件

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    @Override
    public void starting() {
        this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
    }
}

如这个 starting() 方法, 广播了一个 ApplicationStartingEvent 事件给监听器们, 我们直接康康这个广播方法:

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
    @Override
    public void multicastEvent(ApplicationEvent event) {
        multicastEvent(event, resolveDefaultEventType(event));
    }

    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        Executor executor = getTaskExecutor();
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                invokeListener(listener, event);
            }
        }
    }

    private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
        return ResolvableType.forInstance(event);
    }
}
  1. 第一个 multicastEvent 方法中, 在广播事件前, 调用了 resolveDefaultEventType 方法对事件 Pack 了一下, 这里不详细嗦了
  2. 获取一下任务执行器
  3. getApplicationListeners 获取对事件感兴趣的监听器, 参见 1.2.1.1
  4. 如果任务执行器不为空, 则交给任务执行器线程运行监听器, 反之则在主线程运行
  5. 顺着 invoke 的方法往里走可以发现最后只是调用了监听器的 onApplicationEvent 方法 listener.onApplicationEvent(event);
1.2.1.1) getApplicationListeners

这是 SpringBoot 监听器相关的挺重要的一个方法, 获取对事件感兴趣的监听器集合, 先来康康源码:

public abstract class AbstractApplicationEventMulticaster
        implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware {
    protected Collection<ApplicationListener<?>> getApplicationListeners(
            ApplicationEvent event, ResolvableType eventType) {

        Object source = event.getSource();
        Class<?> sourceType = (source != null ? source.getClass() : null);
        ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

        // Quick check for existing entry on ConcurrentHashMap...
        ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
        if (retriever != null) {
            return retriever.getApplicationListeners();
        }

        if (this.beanClassLoader == null ||
                (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
                        (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
            // Fully synchronized building and caching of a ListenerRetriever
            synchronized (this.retrievalMutex) {
                retriever = this.retrieverCache.get(cacheKey);
                if (retriever != null) {
                    return retriever.getApplicationListeners();
                }
                retriever = new ListenerRetriever(true);
                Collection<ApplicationListener<?>> listeners =
                        retrieveApplicationListeners(eventType, sourceType, retriever);
                this.retrieverCache.put(cacheKey, retriever);
                return listeners;
            }
        }
        else {
            // No ListenerRetriever caching -> no synchronization necessary
            return retrieveApplicationListeners(eventType, sourceType, null);
        }
    }
}
  1. 根据传入事件, 生成对应的 CacheKey 缓存 key
  2. 尝试用 CacheKey 缓存中拿 retriever 缓存的监听器列表, 如果之前有缓存则直接返回
  3. 校验一下事件, 然后进入同步代码块, 仔细康康可以看出这里用了双重校验锁, 单例里常见的那个
    1. 再尝试从缓存中取, 以免重复 Put
    2. 使用 retrieveApplicationListeners 函数获取对传入事件感兴趣的监听器列表, 代码太长就不贴了, 简单说一下实现
      1. 遍历 SpringBoot 初始化过程加载的所有监听器, 大致可分为两类监听器, SmartApplicationListenerApplicationListener
      2. SmartApplicationListener 则调用其 supportsEventType 方法判断是否支持此事件, ApplicationListener 则通过获取泛型来判断是否支持该事件
      3. 将支持该事件的监听器集合返回
    3. Put 进缓存中

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

1) 监听器模式

SpringBoot 的监听器可以在程序运行中,通过捕获到感兴趣的事件而触发,从而对 ApplicationContext 或程序做一些事情。

首先先康康精简版的 SpringBoot 监听器模式,监听我们的天气事件:

1.1) 事件

public abstract class WeatherEvent {
  public abstract String getWeather();
}

public class WeatherRainEvent extends WeatherEvent {
  @Override
  public String getWeather() {
    return "Raining...";
  }
}

public class WeatherSnowEvent extends WeatherEvent {
  @Override
  public String getWeather() {
    return "Snowing...";
  }
}

1.2) 监听器

public interface WeatherListener {
  void onEvent(WeatherEvent event);
}

public class WeatherRainListener implements WeatherListener {
  @Override
  public void onEvent(WeatherEvent event) {
    if (!(event instanceof WeatherRainEvent)) return;
    System.out.println(event.getWeather());
  }
}

public class WeatherSnowListener implements WeatherListener {
  @Override
  public void onEvent(WeatherEvent event) {
    if (!(event instanceof WeatherSnowEvent)) return;
    System.out.println(event.getWeather());
  }
}

1.3) 广播器

public class WeatherBroadcast {

  private final List<WeatherListener> listeners = new ArrayList<>();

  public void broadcast(WeatherEvent event) {
    listeners.forEach(listener -> listener.onEvent(event));
  }

  public void addListener(WeatherListener listener) {
    listeners.add(listener);
  }

  public void removeListener(WeatherListener listener) {
    listeners.remove(listener);
  }
}

1.4) 测试

public class WeatherBroadcastTest {

  public static void main(String[] args) {
    WeatherRainListener weatherRainListener = new WeatherRainListener();
    WeatherSnowListener weatherSnowListener = new WeatherSnowListener();

    WeatherBroadcast weatherBroadcast = new WeatherBroadcast();
    weatherBroadcast.addListener(weatherRainListener);
    weatherBroadcast.addListener(weatherSnowListener);

    WeatherRainEvent weatherRainEvent = new WeatherRainEvent();
    WeatherSnowEvent weatherSnowEvent = new WeatherSnowEvent();
    weatherBroadcast.broadcast(weatherRainEvent);
    weatherBroadcast.broadcast(weatherSnowEvent);
  }
}

2) 定义监听器

SpringBoot 中大致可分为两类监听器,SmartApplicationListenerApplicationListener

  • SmartApplicationListener: 可实现 supportsEventType 方法实现对多种事件复杂的监听。
  • ApplicationListener: 可通过指定泛型,监听简单的单一事件。

SpringBoot 在运行中会产生下图中的事件,我们可以监听这些事件来实现我们的监听器:
SpringApplicationEvent.png

public class FirstListener implements ApplicationListener<ApplicationStartedEvent> {
  @Override
  public void onApplicationEvent(ApplicationStartedEvent event) {
    ConfigurableApplicationContext applicationContext = event.getApplicationContext();
    System.out.println("FirstListener......");
  }
}
public class SecondListener implements SmartApplicationListener {
  @Override
  public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
    return eventType.isAssignableFrom(ApplicationStartedEvent.class)
        || eventType.isAssignableFrom(ApplicationFailedEvent.class);
  }

  @Override
  public void onApplicationEvent(ApplicationEvent event) {
    System.out.println("SecondListener......");
  }
}

3) 注入监听器

注入监听器的方法和注入初始化器差不多,也有三种方法

3.1) 定义 /META-INF/spring.factories 注入

如果翻阅 SpringBoot 源码会得知启动过程中会从 /META-INF/spring.factories 中取得监听器并执行,spring-boot.jar
spring-boot-autoconfigure.jar 中有此配置文件例子,我们可以参考它们的格式编写我们的 spring.factories
,放进 classpath 下的 META-INF 中:

# Application Listener, 需添加多个可用 ',' 分隔
org.springframework.context.ApplicationListener=\
cat.wars.course.multidb.listener.listener.FirstListener,\
cat.wars.course.multidb.listener.listener.SecondListener

3.2) 通过 SpringApplication 注入

通常我们的 SpringBoot 启动主函数是这样写的:

SpringApplication.run(Application.class, args);

可以拆分为两行:

SpringApplication application = new SpringApplication(Application.class);
application.run(args);

可以使用 SpringApplication.addListeners 方法添加我们的监听器

SpringApplication application = new SpringApplication(Application.class);
application.addListeners(new FirstListener());
application.addListeners(new SecondListener());
application.run(args);

3.3) 配置文件注入

spring-boot.jar 中的 spring.factories 中定义加载了 DelegatingApplicationListener
,其中会读取配置文件中 context.listener.classes 属性,所以我们还能通过定义配置文件注入。

context:
  listener:
    classes: cat.wars.course.multidb.listener.listener.FirstListener,cat.wars.course.multidb.listener.listener.SecondListener

4) 总结

SpringBoot 中至少有三种方法,可以注入我们定义的 Spring 监听器,分别是:

  • 定义 /META-INF/spring.factories 注入
  • 通过 SpringApplicationaddListeners 方法注入
  • 通过配置文件中 context.listener.classes 属性注入

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

简介

ApplicationContextInitializer 是啥呢,我们来看一下源码注释中的介绍:

 * Callback interface for initializing a Spring {@link ConfigurableApplicationContext}
 * prior to being {@linkplain ConfigurableApplicationContext#refresh() refreshed}.
 * 它是 Spring 初始化过程中的一个回调接口,会在 ConfigurableApplicationContext 的 refresh 方法执行前调用

所以我们可以在 SpringBoot 的这个回调点,去对 ApplicationContext 做一些事情 =。=
我们有三种方法让 SpringBoot 去加载它的实现:

0) 定义初始化器

我们可以实现 ApplicationContextInitalizer 来定义我们的初始化器,先来看一哈这个接口源码:

/**
 * Callback interface for initializing a Spring {@link ConfigurableApplicationContext}
 * prior to being {@linkplain ConfigurableApplicationContext#refresh() refreshed}.
 *
 * <p>Typically used within web applications that require some programmatic initialization
 * of the application context. For example, registering property sources or activating
 * profiles against the {@linkplain ConfigurableApplicationContext#getEnvironment()
 * context's environment}. See {@code ContextLoader} and {@code FrameworkServlet} support
 * for declaring a "contextInitializerClasses" context-param and init-param, respectively.
 *
 * <p>{@code ApplicationContextInitializer} processors are encouraged to detect
 * whether Spring's {@link org.springframework.core.Ordered Ordered} interface has been
 * implemented or if the @{@link org.springframework.core.annotation.Order Order}
 * annotation is present and to sort instances accordingly if so prior to invocation.
 *
 * @author Chris Beams
 * @since 3.1
 * @param <C> the application context type
 * @see org.springframework.web.context.ContextLoader#customizeContext
 * @see org.springframework.web.context.ContextLoader#CONTEXT_INITIALIZER_CLASSES_PARAM
 * @see org.springframework.web.servlet.FrameworkServlet#setContextInitializerClasses
 * @see org.springframework.web.servlet.FrameworkServlet#applyInitializers
 */
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

    /**
     * Initialize the given application context.
     * @param applicationContext the application to configure
     */
    void initialize(C applicationContext);
}

实现它需要一个泛型,可以传入 ConfigurableApplicationContext 的实现,我们可以根据需要选择对应的实现,一般直接用它本身即可。
且可以使用 @Order 调整加载的顺序,这里仅演示注入一个 Environment:

@Order(1)
public class InjectInitializer
    implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
    Map<String, Object> propertyMap = Map.of("InjectKey", "InjectValue");
    MapPropertySource propertySource = new MapPropertySource("InjectProperty", propertyMap);
    applicationContext.getEnvironment().getPropertySources().addLast(propertySource);
  }
}

1) 定义 /META-INF/spring.factories 注入

如果翻阅 SpringBoot 源码会得知启动过程中会从 /META-INF/spring.factories 中取得加载器并执行
spring-boot.jarspring-boot-autoconfigure.jar 中有此配置文件例子,我们可以参考它们的格式编写我们的 spring.factories
,放进 classpath 下的 META-INF 中:

# Application Context Initializers, 需添加多个可用 ',' 分隔
org.springframework.context.ApplicationContextInitializer=\
cat.wars.inject.InjectInitializer,\
cat.wars.inject.Inject2Initializer

2) 通过 SpringApplication 注入

通常我们的 SpringBoot 启动主函数是这样写的:

SpringApplication.run(Application.class, args);

可以拆分为两行:

SpringApplication application = new SpringApplication(Application.class);
application.run(args);

可以使用 SpringApplication.addInitializers 方法添加我们的初始化器

SpringApplication application = new SpringApplication(Application.class);
application.addInitializers(new InjectInitializer());
application.run(args);

3) 配置文件注入

spring-boot.jar 中的 spring.factories 中定义加载了 DelegatingApplicationContextInitializer
,其中会读取配置文件中 context.initializer.classes 属性,所以我们还能通过定义配置文件注入。

context:
  initializer:
    classes: cat.wars.course.multidb.initial.InjectInitializer,cat.wars.course.multidb.initial.Inject2Initializer

4) 总结

SpringBoot 中至少有三种方法,可以注入我们定义的 Spring 上下文初始化器,分别是:

  • 定义 /META-INF/spring.factories 注入
  • 通过 SpringApplicationaddInitializers 方法注入
  • 通过配置文件中 context.initializer.classes 属性注入

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

源码上传至 Github: https://github.com/WarsFeng/SpringBoot_MultiDB

1) 前置知识点

1.1) 注解

注解仅为一个标识,起到注释或辅助程序运行的作用。

1.1.1) Meta Annotation

Java 提供了四个标准元注解,用于说明自定义注解的作用域

  • @Target: 注解使用范围,Type \ Field \ Method \ Parameter ...
  • @Retention: 存在于哪个阶段,Source \ Class \ Runtime
  • @Document: 包含进 JavaDoc
  • @Inherited: 标识的注解被子类继承
    • 有一例 @A,注解了 Class C,Class C2 extend C,则 C2 也拥有 @A

2) SpringBoot 实现读写分离

一般结合 MyBatis 使用,实现多数据源的支持。

2.1) Config

首先在 application.yml 中定义两个数据源 reader, writer:

spring:
  datasource:
    reader:
      url: jdbc:mysql://rcat:3306/multi_db?useUnicode=true@characterEncoding=utf-8
      username: root
      password: passwd
      driver-class-name: com.mysql.cj.jdbc.Driver
    writer:
      url: jdbc:mysql://rcat:3306/multi_db2?useUnicode=true@characterEncoding=utf-8
      username: root
      password: passwd
      driver-class-name: com.mysql.cj.jdbc.Driver

2.2) Enum and Annotation

我们将使用注解切换数据源,定义我们的枚举 DataSourceType 和注解 DBSelector
, 注解为 Method 级别注解,实现函数内的数据源选择。

public enum DataSourceType {
  READER,
  WRITER
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DBSelector {

  DataSourceType value() default DataSourceType.READER;
}

2.3) ThreadLocal handler

使用 ThreadLocal 保证代码优雅,定义数据源绑定器,下文会利用 AOP 将 dbType set 进 ThreadLocal

public class DataSourceBinding {

  private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

  public static void bindDataSource(DataSourceType dbType) {
    threadLocal.set(dbType.getValue());
  }

  public static String getDataSource() {
    return threadLocal.get();
  }

  public static void clearDataSource() {
    threadLocal.remove();
  }
}

2.4) Cut Aspect

本文仅实现 Method 级别的数据源切换,为 @DBSelector 标注域内绑定上 dbType
, 方法开始时 bind,结束后 clear

@Aspect
@Order(1)
@Component
public class MultiDataSourceAspect {

  @Around("@annotation(cat.wars.course.multidb.annotation.DBSelector)")
  public Object around(ProceedingJoinPoint point) throws Throwable {
    MethodSignature signature = (MethodSignature) point.getSignature();
    DBSelector dbSelector = signature.getMethod().getAnnotation(DBSelector.class);
    if (null != dbSelector) {
      DataSourceBinding.bindDataSource(dbSelector.value().name());
    }

    try {
      return point.proceed();
    } finally {
      DataSourceBinding.clearDataSource();
    }
  }
}

2.5) Configuration

这是最重要的一个部分,Spring-JDBC 内置了多数据源的支持,基于 DataSource 实现了 AbstractRoutingDataSource 接口。

原理:AbstractRoutingDataSource 内置了一个 Map 可用于存储我们的多个数据源, getConnection 时会从 Map 中尝试 Get
, 用 determineCurrentLookupKey 返回值当作 Key, 所以我们应该怎么实现这个抽象类呢:

  • 构造函数中将多个数据源 Put 进 Map,使用 DataSourceType 当 Key
  • determineCurrentLookupKey 中直接将 ThreadLocal 中的 dbType 取出即完成~

接着用 @Bean@Primary 直接塞进容器中,这里我直接用内部类写在 Configuratoin 中了 =。=

@Configuration
public class MultiDataSourceConfig {

  @Bean
  @ConfigurationProperties("spring.datasource.reader")
  public DataSource reader() {
    return DataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.writer")
  public DataSource writer() {
    return DataSourceBuilder.create().build();
  }

  @Bean
  @Primary
  public DynamicDataSource dataSource(DataSource reader, DataSource writer) {
    HashMap<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put(DataSourceType.READER.name(), reader);
    targetDataSources.put(DataSourceType.WRITER.name(), writer);

    return new DynamicDataSource(reader, targetDataSources);
  }

  /** 动态数据源 DataSource */
  private static final class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
      this.setDefaultTargetDataSource(defaultDataSource);
      if (null == targetDataSources) return;
      this.setTargetDataSources(targetDataSources);
      this.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
      return DataSourceBinding.getDataSource();
    }
  }
}

接着由于 SpringBoot 的默认配置会在启动时使用 DataSourceAutoConfiguration 获取配置中的 spring.datasource 初始化默认的 DataSource
, 我们需要在 SpringBoot 的启动器中排除掉它,如:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MultiDbApplication {

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

2.6) Test

测试!这里用 SpringMVC 测试效果:

@RestController
public class ReadWriteController {

  private final JdbcTemplate jdbcTemplate;

  public ReadWriteController(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @GetMapping("/read")
  @DBSelector(DataSourceType.READER)
  public String read() {
    return jdbcTemplate.queryForList("SELECT * FROM user").toString();
  }

  @GetMapping("/write")
  @DBSelector(DataSourceType.WRITER)
  public String write() {
    return jdbcTemplate.queryForList("SELECT * FROM t_user").toString();
  }
}

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.LinkedBlockingQueue;

@Slf4j
public class CatExecutorService {

  private final LinkedBlockingQueue<Runnable> threadQueue = new LinkedBlockingQueue<>();

  public CatExecutorService() {
    log.info("Starting CatPool ...");
    new Thread(
            () -> {
              while (true) {
                try {
                  Runnable take = threadQueue.take();
                  take.run();
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
              }
            },
            "CatThreadPool")
        .start();
    log.info("Started CatPool.");
  }

  public void submit(Runnable runnable) {
    if (null == runnable) return;
    synchronized (this) {
      threadQueue.add(runnable);
    }
  }
}
Title - Artist
0:00