在編寫分布式微服務(wù)架構(gòu)項目的時候,我們一般在一個idea的project里創(chuàng)建多個獨立的module员辩,有時候我們多個服務(wù)的數(shù)據(jù)庫频轿、連接池的偷仿、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的自動配置已經(jīng)極大的為我們避免了xml地獄衣屏,我們已經(jīng)少寫了很多東西躏升,但如果在每一個module的application.yml中都再寫一遍依然很麻煩,維護起來也不方便狼忱。
如果能把這些都集中起來做成一個starter膨疏,所有需要數(shù)據(jù)庫功能的服務(wù)就可以依賴這個starter實現(xiàn)自動配置,并且如果某個服務(wù)的數(shù)據(jù)庫信息和自動配置的基礎(chǔ)信息不一致藕赞,比如username是root2成肘,starter中的基礎(chǔ)配置是root,那么就只需要在自己的yml中設(shè)置username即可覆蓋starter的基礎(chǔ)配置中的username斧蜕。
本例中我使用的是Druid双霍,其實無論使用哪種連接池,最后應(yīng)該都是通過其自動配置將自己的DataSource實現(xiàn)放入到spring容器中批销,所以spring.datasource下的信息應(yīng)該是在Druid的autoConfigure類中讀取到洒闸,并設(shè)置到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實現(xiàn)均芽, 而且還加了@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
注解丘逸,這時候DataSourceProperties
就已經(jīng)被加載到spring容器中,這個類中就保存著spring.datasource下的信息掀宋,其源碼如下:
@ConfigurationProperties(
prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private String name;
private boolean generateUniqueName;
...
那么實現(xiàn)思路就簡單了深纲,只需要在我們自己的starter中創(chuàng)建一個自動配置類,讓其被加載順序優(yōu)先于Druid的DruidDataSourceAutoConfigure劲妙,并在我們的配置類中搶先一步加載DataSourceProperties湃鹊,判斷下我們starter自己的基礎(chǔ)配置中的信息在DataSourceProperties里是否是空值。
如果是空值镣奋,就代表當前依賴此starter項目的application.yml中沒有填寫此值币呵,我們就可以把starter中的信息set進DataSourceProperties,等到DruidDataSourceAutoConfigure被加載時侨颈,它創(chuàng)建的DataSource就是用的我們搶先修改過的DataSourceProperties余赢,這樣就實現(xiàn)了項目中可以一句配置都不用寫芯义,只要引入了我們的starter依賴就會為其自動配置,而如果項目中寫了配置就又可以覆蓋starter的配置妻柒。
具體實現(xiàn)如下:
@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等信息啤贩,是的我們的確沒有寫待秃,但是我們在創(chuàng)建DruidDataSource之前已經(jīng)把starter的這些信息設(shè)置進DataSourceProperties了,所以url痹屹、driverClass等信息不應(yīng)該為空才對章郁。
經(jīng)過斷點調(diào)試發(fā)現(xiàn),我們DaoAutoConfigure的注解@AutoConfigureBefore({DruidDataSourceAutoConfigure.class})
并沒有起作用志衍,DaoAutoConfigure是在DruidDataSourceAutoConfigure之后被加載暖庄,根據(jù)springboot自動配置原理,我們的DaoAutoConfigure和依賴此starter的項目包名并不一樣楼肪,是不會被@ComponentScan
搗亂加載順序的培廓,而且DaoAutoConfigure和DruidDataSourceAutoConfigure都不是普通的Configuration,都是在spring.factories中注冊過的春叫,順序不應(yīng)該亂才對肩钠。
再次斷點調(diào)試DefaultListableBeanFactory,這個類中的beanDefinitionNames字段保存有排好序所有待spring容器加載的beanName
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
...
private volatile List<String> beanDefinitionNames = new ArrayList(256);
...
}
調(diào)試發(fā)現(xiàn)我們的DaoAutoConfigure的確是排在DruidDataSourceAutoConfigure之前暂殖,順序并沒有亂价匠,但是在真正加載的過程中卻亂了,繼續(xù)斷點跟蹤到DefaultListableBeanFactory的實例化bean的doCreateBean方法發(fā)現(xiàn)呛每,當加載到一個項目中的controller類時踩窖,DruidDataSourceAutoConfigure也被插隊加載了,問題的根源就在這個controller類晨横,代碼如下:
@RestController
public class TestController {
@Autowired
private UserMapper userMapper;
因為TestController類里注入了UserMapper洋腮,而UserMapper會依賴并加載mybatis,mybatis又會依賴并加載DataSource手形,而DataSource又在DruidDataSourceAutoConfigure中創(chuàng)建的啥供,根據(jù)springboot自動配置原理,controller叁幢、service、component加載會優(yōu)先于所有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的自動配置就完全實現(xiàn)了顷帖,剩下的就可以依照此思路接著去寫Druid美旧、Mybatis、PageHelper等其他庫的自動配置了贬墩,我這里就不多啰嗦了榴嗅。
最后:請盡情體驗springboot-starter-autoconfigure帶來的美妙體驗吧