自定义springboot-starter打造通用数据库基础配置思路及实现

在编写分布式微服务架构项目的时候,我们一般在一个idea的project里创建多个独立的module,有时候我们多个服务的数据库、连接池的、mybatis等框架的依赖和配置大部分都可能相同,比如以下依赖和配置:

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/hehehe?serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      max-active: 20
      min-idle: 8
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
pagehelper:
  reasonable: true
  helper-dialect: mysql
...

虽然springboot的自动配置已经极大的为我们避免了xml地狱,我们已经少写了很多东西,但如果在每一个module的application.yml中都再写一遍依然很麻烦,维护起来也不方便。

如果能把这些都集中起来做成一个starter,所有需要数据库功能的服务就可以依赖这个starter实现自动配置,并且如果某个服务的数据库信息和自动配置的基础信息不一致,比如username是root2,starter中的基础配置是root,那么就只需要在自己的yml中设置username即可覆盖starter的基础配置中的username。

本例中我使用的是Druid,其实无论使用哪种连接池,最后应该都是通过其自动配置将自己的DataSource实现放入到spring容器中,所以spring.datasource下的信息应该是在Druid的autoConfigure类中读取到,并设置到Druid的DataSource中,查看DruidDataSourceAutoConfigure源码如下:

@Configuration
@ConditionalOnClass({DruidDataSource.class})
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class, DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {
    private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);

    public DruidDataSourceAutoConfigure() {
    }

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

可以看到DruidDataSourceAutoConfigure向spring容器中添加了Druid的DataSource实现, 而且还加了@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})注解,这时候DataSourceProperties就已经被加载到spring容器中,这个类中就保存着spring.datasource下的信息,其源码如下:

@ConfigurationProperties(
    prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    private ClassLoader classLoader;
    private String name;
    private boolean generateUniqueName;
...

那么实现思路就简单了,只需要在我们自己的starter中创建一个自动配置类,让其被加载顺序优先于Druid的DruidDataSourceAutoConfigure,并在我们的配置类中抢先一步加载DataSourceProperties,判断下我们starter自己的基础配置中的信息在DataSourceProperties里是否是空值。
如果是空值,就代表当前依赖此starter项目的application.yml中没有填写此值,我们就可以把starter中的信息set进DataSourceProperties,等到DruidDataSourceAutoConfigure被加载时,它创建的DataSource就是用的我们抢先修改过的DataSourceProperties,这样就实现了项目中可以一句配置都不用写,只要引入了我们的starter依赖就会为其自动配置,而如果项目中写了配置就又可以覆盖starter的配置。
具体实现如下:

@Configuration
@ConditionalOnClass({DruidDataSource.class})
@AutoConfigureBefore({DruidDataSourceAutoConfigure.class})
@EnableConfigurationProperties({DataSourceProperties.class, DaoProperties.class})
public class DaoAutoConfigure {

    public DaoAutoConfigure(DataSourceProperties dataSourceProperties, DaoProperties daoProperties) {
        if (Strings.isBlank(dataSourceProperties.getUrl())) {
            dataSourceProperties.setUrl(daoProperties.getJdbc().getUrl());
        }
        if (Strings.isBlank(dataSourceProperties.getDriverClassName())) {
            dataSourceProperties.setDriverClassName(daoProperties.getJdbc().getDriverClassName());
        }
        if (Strings.isBlank(dataSourceProperties.getUsername())) {
            dataSourceProperties.setUsername(daoProperties.getJdbc().getUsername());
        }
        if (Strings.isBlank(dataSourceProperties.getPassword())) {
            dataSourceProperties.setPassword(daoProperties.getJdbc().getPassword());
        }
        if (dataSourceProperties.getType() == null) {
            dataSourceProperties.setType(daoProperties.getJdbc().getType());
        }
         ...
    }
}

@AutoConfigureBefore({DruidDataSourceAutoConfigure.class})注解代表在DruidDataSourceAutoConfigure之前加载。

但是实际只这么写是有问题的,当项目依赖此starter后启动时会报以下错误:

2019-01-16 18:51:02.803 ERROR 20272 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
    If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
    If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

这个错误比较容易理解,大致意思是说不能配置DataSource,因为我们没有提供url、driverClass等信息,是的我们的确没有写,但是我们在创建DruidDataSource之前已经把starter的这些信息设置进DataSourceProperties了,所以url、driverClass等信息不应该为空才对。

经过断点调试发现,我们DaoAutoConfigure的注解@AutoConfigureBefore({DruidDataSourceAutoConfigure.class})并没有起作用,DaoAutoConfigure是在DruidDataSourceAutoConfigure之后被加载,根据springboot自动配置原理,我们的DaoAutoConfigure和依赖此starter的项目包名并不一样,是不会被@ComponentScan捣乱加载顺序的,而且DaoAutoConfigure和DruidDataSourceAutoConfigure都不是普通的Configuration,都是在spring.factories中注册过的,顺序不应该乱才对。

再次断点调试DefaultListableBeanFactory,这个类中的beanDefinitionNames字段保存有排好序所有待spring容器加载的beanName

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
...
    private volatile List<String> beanDefinitionNames = new ArrayList(256);
...
}

调试发现我们的DaoAutoConfigure的确是排在DruidDataSourceAutoConfigure之前,顺序并没有乱,但是在真正加载的过程中却乱了,继续断点跟踪到DefaultListableBeanFactory的实例化bean的doCreateBean方法发现,当加载到一个项目中的controller类时,DruidDataSourceAutoConfigure也被插队加载了,问题的根源就在这个controller类,代码如下:

@RestController
public class TestController {
    @Autowired
    private UserMapper userMapper;

因为TestController类里注入了UserMapper,而UserMapper会依赖并加载mybatis,mybatis又会依赖并加载DataSource,而DataSource又在DruidDataSourceAutoConfigure中创建的,根据springboot自动配置原理,controller、service、component加载会优先于所有autoConfigure,所以就导致了AutoConfigureBefore的失效,DruidDataSourceAutoConfigure进行了弯道超车。

知道了这一点解决起来就很简单了,由我们的DaoAutoConfigure接手向容器中注入DataSource就可以了,而且DruidDataSourceAutoConfigure的

    @ConditionalOnMissingBean
    public DataSource dataSource() {

添加了ConditionalOnMissingBean注解,也不会重复注入,修改后的DaoAutoConfigure如下:

@Configuration
@ConditionalOnClass({DruidDataSource.class})
@AutoConfigureBefore({DruidDataSourceAutoConfigure.class})
@EnableConfigurationProperties({DataSourceProperties.class, DaoProperties.class})
public class DaoAutoConfigure {

    public DaoAutoConfigure(DataSourceProperties dataSourceProperties, DaoProperties daoProperties) {
        if (Strings.isBlank(dataSourceProperties.getUrl())) {
            dataSourceProperties.setUrl(daoProperties.getJdbc().getUrl());
        }
        if (Strings.isBlank(dataSourceProperties.getDriverClassName())) {
            dataSourceProperties.setDriverClassName(daoProperties.getJdbc().getDriverClassName());
        }
        if (Strings.isBlank(dataSourceProperties.getUsername())) {
            dataSourceProperties.setUsername(daoProperties.getJdbc().getUsername());
        }
        if (Strings.isBlank(dataSourceProperties.getPassword())) {
            dataSourceProperties.setPassword(daoProperties.getJdbc().getPassword());
        }
        if (dataSourceProperties.getType() == null) {
            dataSourceProperties.setType(daoProperties.getJdbc().getType());
        }
    }

    @Bean(initMethod = "init")
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        return DruidDataSourceBuilder.create().build();
    }
}

到此DataSource的自动配置就完全实现了,剩下的就可以依照此思路接着去写Druid、Mybatis、PageHelper等其他库的自动配置了,我这里就不多啰嗦了。

最后:请尽情体验springboot-starter-autoconfigure带来的美妙体验吧


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容