基于Spring Boot的数据库初始化(二)

Java 1 6683

基于Spring Boot的数据库初始化(二)

本篇文章将介绍如何使用一些hack手段和巧妙的思路来动态的开启关闭数据库初始化功能, 并在连接池连接数据库之前将数据库初始化好(我也很无奈, Spring没提供这个功能).在上篇博文基于Spring Boot的数据库初始化(一)中介绍了如何使用Spring Boot配置数据库的初始化功能. 但这个配置是静态的.

实现动态初始化的基本思路

在配置文件中配置好初始化的一些必要设置, 然后在程序启动时读取此设置, 再根据某个状态来判断决定是否需要执行初始化数据库的操作, 然后动态的修改这个初始配置.

简化版配置文件片段

spring.datasource:
  initialization-mode: always # 这个值就是要动态修改的值
  schema-username: root
  schema-password: root
  sql-script-encoding: UTF-8
  schema:
    - classpath:db/schema-mysql.sql # 初始化数据库的SQL脚本
  type: com.zaxxer.hikari.HikariDataSource # 初始化数据库时使用的连接池
  url: jdbc:mysql://127.0.0.1:3306/sys #  初始化时可以连接到Mysql的系统数据库
  druid:
    url: jdbc:mysql://127.0.0.1:3306/lore_blog
    username: root
    password: root
  ...  

配置项的具体作用可以查看上一篇博文基于Spring Boot的数据库初始化(一) 其中initialization-mode配置项就是我们要动态修改的地方, 以达到动态初始化的目的. 所以我们就要想办法获取并修改它, 并且是在这个配置生效前就完成它的修改.

获取并修改指定配置项

首先选择一个合适的时机完成它的修改. 一般配置文件中的各个配置项都是用来对各个bean进行自定义配置的, 所以要选在spring创建bean之前. 我们可用利用两个Spring Application events:

 // is sent when the Environment to be used in the context is known but before the context is created.
ApplicationEnvironmentPreparedEvent
 // is sent just before the refresh is started but after bean definitions have been loaded.
ApplicationPreparedEvent

通过创建 Event 对应的 Listener 然后在 Listener 中获取 Environment中的配置项就可以完成修改了. 建议选择 ApplicationEnvironmentPreparedEvent 因为这个事件类中有一个 getEnvironment() 方法可以更方便的获取 Environment.

简化版样例:

public class DatabaseInitializer implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        String initializationModeKey = "spring.datasource.initialization-mode";

        ConfigurableEnvironment environment = event.getEnvironment();
        if (!environment.containsProperty(initializationModeKey)) {
            return;
        }

        Map propertyMap = null;
        MutablePropertySources propertySources = environment.getPropertySources();
        for (PropertySource<?> propertySource : propertySources) {
            if (propertySource.containsProperty(propertyKey)) {
                Object source = propertySource.getSource();
                if (source instanceof Map) {
                    propertyMap = (Map) source;
                    break;
                }
            }
        }

        if (propertyMap != null) {
          if (不需要初始化) {
            // 关闭初始化
            propertyMap.put(initializationModeKey, DataSourceInitializationMode.NEVER);
          }
        }
    }

}

通过以上方法就可以根据某一状态来决定是否需要初始化数据库了. 注: 以上修改方法限 spring boot version 2.2.0.M3 之前的版本, 因为下一个版本中放置配置项的 Map 被处理重成了 UnmodifiableMap, 是无法修改的, 修改就会抛出 java.lang.UnsupportedOperationException: null 错误. 我想出解决办法后会写文章分享. 目前有两个思路解决不过是否可行还未知

解决另一个问题

什么问题

DataSourceInitializerPostProcessor.java这个类负责在 DataSource 初始化后调用DataSourceInitializerInvoker.java 开始数据库的初始化过程.

相关代码片段:

  @Override
	public Object postProcessAfterInitialization(Object bean, String beanName)
			throws BeansException {
		if (bean instanceof DataSource) {
			// force initialization of this bean as soon as we see a DataSource
			this.beanFactory.getBean(DataSourceInitializerInvoker.class);
		}
		return bean;
	}

SPring boot 的数据库初始化工作是在连接池初始化后才开始的. 那么这里就可能产生一个问题, 比如我的 lore-blog 博客系统, 博客所需的数据库是用初始化脚本创建出来的, 那么在执行初始化之前就初始化连接池的话因为数据库还没有创建所以会出错的, 我需要在连接池初始化之间就必须执行数据库的初始化功能将博客需要的数据库和数据创建出来.(真的无法领会 spring 这个我认为NC的设计, 现在的实现方式大大缩小了数据库初始化功能的应用场景. 不知有什么难言之隐?)

解决方法

核心思路是连接池bean被spring创建后, 延迟执行连接池自身的初始化部分. 如何延迟呢? 我自创了 "狸猫换太子" 大法(后面详细解释). 由于, 我的博客系统使用的连接池是druid, 初始化数据库用的连接池是HikariCP(据说Java中性能最好的连接池. 如果并不注重监控功能那么我建议使用 HikariCP 而不是 druid. 顺便吐槽下 druid, 因最近研究 druid 中的 SQL AST部分, 那代码写的我都不敢开启 idea 的"阿里编码规约"插件. 我用他自家的标准去评价他应该没毛病吧. ) 所以, 我下面的解决方法是建立在使用 druid 连接池的基础上(至于初始化用的什么连接池无所谓, 但是别都用同一个连接池), 其它连接池可能要视情况做调整.

花研究了spring的源码, 不过我始终没有找到一个能完全在创建连接池bean之前就执行初始化数据库的方法. 不过在spring创建创建连接池bean之后和连接池自身的初始化这个过程中还是让我找到了可乘之机.

druid 连接池的自动配置类DruidDataSourceAutoConfigure.java代码片段:

@Bean(initMethod = "init")
   @ConditionalOnMissingBean
   public DataSource dataSource() {
       LOGGER.info("Init DruidDataSource");
       return new DruidDataSourceWrapper();
   }

dataSource bean被创建后, 在执行 DruidDataSource#init()方法, 也就是连接池自身的初始化方法时才连接的数据库. 所以我们在这个 init 方法执行之前执行数据库初始化部分就可以了. 先用文字简要描述实现过程: 注册一个自定义 BeanPostProcessor 用这个后置处理器在执行DruidDataSource#init()方法前, 将 DruidDataSource 连接池bean, 换成另一个假的连接池bean来代替 DruidDataSource 执行 init() 方法. 然后等数据库初始化完成之后在将原来的 DruidDataSource 替换回来并直接调用 init() 方法继续正常完成连接池的初始化.

show code:

@Configuration
@Import(JdbcConfig.Registrar.class)
@AutoConfigureBefore(DruidDataSourceAutoConfigure.class)
@ConditionalOnProperty(name = "spring.datasource.initialization-mode")
public class JdbcConfig {

    static class Registrar implements ImportBeanDefinitionRegistrar {

        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                            BeanDefinitionRegistry registry) {
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(LazyDataSourceInitPostProcessor.class);
            beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
            beanDefinition.setSynthetic(true);
            registry.registerBeanDefinition("lazyDataSourceInitPostProcessor", beanDefinition);
        }
    }

    /**
     * 推迟{@link DruidDataSource#init()}方法的执行, 以确保数据库被初始化后再初始化连接池.
     */
    static class LazyDataSourceInitPostProcessor implements BeanPostProcessor, Ordered {

        @Override
        public int getOrder() {
            // 比 Spring 的 DataSourceInitializerPostProcessor.java 低一个优先级
            return Ordered.HIGHEST_PRECEDENCE + 2;
        }

        @Override
        public Object postProcessBeforeInitialization(@Nullable Object bean, String beanName)
                throws BeansException {
            if (bean instanceof DruidDataSource) {
                // 用一个假的连接池代替 DruidDataSource (狸猫换太子)
                return new FakeDataSource(bean);
            }
            return bean;
        }

        @Override
        public Object postProcessAfterInitialization(@Nullable Object bean, String beanName)
                throws BeansException {
            if (bean instanceof FakeDataSource) {
                FakeDataSource fakeDataSource = (FakeDataSource) bean;
                Object object = fakeDataSource.getObject();
                if (object instanceof DruidDataSource) {
                    DruidDataSource druidDataSource = (DruidDataSource) object;
                    try {
                        // Spring 的 DataSourceInitializerInvoker.java 已经完成好了数据库的初始化
                        // 工作, 可以真正的初始化连接池了
                        druidDataSource.init();
                    } catch (SQLException e) {
                        throw new BeanInitializationException("Druid initialization exception.", e);
                    }
                }
                // 换回真正的 DruidDataSource
                return object;
            }

            return bean;
        }
    }

    static class FakeDataSource extends AbstractDataSource {

        // 存放原连接池对象
        private Object object;

        private FakeDataSource(Object object) {
            this.object = object;
        }

        public void init() {
            // 什么也不做, 就是为了代替原连接池的init方法被调用
        }

        public Object getObject() {
            return object;
        }


        @Override
        public Connection getConnection() {
            return null;
        }

        @Override
        public Connection getConnection(String username, String password) {
            return null;
        }
    }

}

本文采用 知识共享署名4.0国际许可协议进行许可. 本站文章除注明转载/出处外, 均为本站原创或翻译,转载前请务必署名!

  1. cai

    建议覆盖(addFirst)properties,而不是修改 ConfigurableEnvironment.getPropertySources().addFirst(new MapPropertySource(...));

    回复