在编写分布式微服务架构项目的时候,我们一般在一个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: 8mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truepagehelper: 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 classAction: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类,代码如下:
@RestControllerpublic 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带来的美妙体验吧
联系客服