前文讲到了SpringBoot如何实现自动配置,SpringBoot的自动配置极大的提升了框架的使用效率。今天我们就来说一说 面试必问 之如何手写starter。
本文以C3P0连接池为基础,实现一个C3P0-Starter。
首先给大家介绍一些SpringBoot默认支持的部分连接池,看看它的内部是怎么实现的,咱们依葫芦画瓢,干他一个Starter。
spring-boot-autoconfigure:SpringBot的自动配置依赖,这一票能不能干成全看这个东西。
咱们直奔主题找到(数据源自动配置类)
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 复制代码
看看这些东西是不是感觉到好像有点意思?还没有?那我们接着看。
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
@Configuration
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
复制代码
@Configuration Spring配置类等同于.xml
@ConditionalOnClass表示只有classpath中能找到DataSource.class和EmbeddedDatabaseType.class时,DataSourceAutoConfiguration这个类才会被spring容器实例化。我们有时候只需要在pom.xml添加某些依赖包,某些功能就自动打开了,正是这个注解帮我们处理了。
@EnableConfigurationProperties引入了DataSourceProperties的配置。这是个好东西以后写框架一定能用上,看看代码吧。
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private String name;
private boolean generateUniqueName;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
private DataSourceInitializationMode initializationMode = DataSourceInitializationMode.EMBEDDED;
private String platform = "all";
private List<String> schema;
private String schemaUsername;
private String schemaPassword;
private List<String> data;
private String dataUsername;
private String dataPassword;
private boolean continueOnError = false;
private String separator = ";";
private Charset sqlScriptEncoding;
复制代码
看到这里你应该知道咱们平时在.yml或.properties里面的配置项是怎么来的以及它注入到哪些属性上了吧。
@ConfigurationProperties 将配置文件中的配置,以属性的形式自动注入到实体中,要特别说明的一个注属性 ignoreUnknownFields = false 这个超好用,自动检查配置文件中的属性是否存在,不存在则在启动时就报错。
@Import 默认引入了DataSourceConfiguration的几个内部类 Hikari、Tomcat、Dbcp2、Generic
/**
* Tomcat Pool DataSource configuration.
*/
@ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource", matchIfMissing = true)
static class Tomcat extends DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.tomcat")
public org.apache.tomcat.jdbc.pool.DataSource dataSource(
DataSourceProperties properties) {
org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(
properties, org.apache.tomcat.jdbc.pool.DataSource.class);
DatabaseDriver databaseDriver = DatabaseDriver
.fromJdbcUrl(properties.determineUrl());
String validationQuery = databaseDriver.getValidationQuery();
if (validationQuery != null) {
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(validationQuery);
}
return dataSource;
}
}
/**
* Hikari DataSource configuration.
*/
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)
static class Hikari extends DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties,
HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
/**
* DBCP DataSource configuration.
*/
@ConditionalOnClass(org.apache.commons.dbcp2.BasicDataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.commons.dbcp2.BasicDataSource", matchIfMissing = true)
static class Dbcp2 extends DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.dbcp2")
public org.apache.commons.dbcp2.BasicDataSource dataSource(
DataSourceProperties properties) {
return createDataSource(properties,
org.apache.commons.dbcp2.BasicDataSource.class);
}
}
/**
* Generic DataSource configuration.
*/
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type")
static class Generic {
@Bean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
复制代码
@ConditionalOnMissingBean(HikariDataSource.class) 这里的Hikari是SpringBoot的亲儿子所以是默认支持的,在此之前还没有实例化HikariDataSource,往下走(如果我们在应用中主动实现了datasource,那默认的hikari就不会再实现了。
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) 当配置 spring.datasource.type=com.zaxxer.hikari.HikariDataSource或该配置不存在时则程序继续执行。
给大家送上一张条件注解归总图,不熟悉的话可以作为参考,无需多言看我操作。
号外!号外!天啦噜~ 隔壁程序员又写一堆Bug了,大家赶紧去吐槽啊!
代码已经托管在Github平台: V-LoggingTool 。 欢迎大家 start/fork !:star:
这个项目主要目的是供大家学习如何 手写SpringBoot的Starter 、 AOP日志场景处理 、SpringBoot 异步技术 等。同时该类库也可以快速开发小项目的审计日志功能,提供给需要的同学相关的使用方法,达到 开箱即用 的目的,详情请看Readme。
拉回来,下面咱么接着唠!
目的: 手写C3P0的Starter !别忘了咱们是要实现这个东西。
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
</properties>
<dependencies>
<!--------------------------------------------------------->
<!-- 自动配置,那些条件注解才可以使用,最好设置为 optional 为 true 不往下传递-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- C3P0连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.3</version>
</dependency>
<!---------------------------可要可不要------------------------------>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
复制代码
其中 spring-boot-autoconfigure 和c3p0是必须引入的,对于其他的依赖我不再强调,我觉得你应该知道是干什么的,哈哈。我的代码里面用到了@Slf4j这个是使用的web的starter里面的log实现,大家可以忽略。
@Slf4j
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(C3P0DataSourceProperties.class)
public class C3p0DataSourceAutoConfiguration {
@Autowired
C3P0DataSourceProperties dataSourceProperties;
@Bean("c3p0Pool")
@ConditionalOnMissingBean
public DataSource dataSource() throws Exception {
log.info("Start initializing c3p0 connection pool......");
// 创建一个 c3p0 的连接池
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass(dataSourceProperties.getDriver());
dataSource.setJdbcUrl(dataSourceProperties.getJdbcUrl());
dataSource.setUser(dataSourceProperties.getUsername());
dataSource.setPassword(dataSourceProperties.getPassword());
// 池大小配置
dataSource.setAcquireIncrement(dataSourceProperties.getAcquireIncrement());
dataSource.setInitialPoolSize(dataSourceProperties.getInitialPoolSize());
dataSource.setMinPoolSize(dataSourceProperties.getMinPoolSize());
dataSource.setMaxPoolSize(dataSourceProperties.getMaxPoolSize());
dataSource.setMaxIdleTime(dataSourceProperties.getMaxIdleTime());
dataSource.setMaxStatements(dataSourceProperties.getMaxStatements());
dataSource.setIdleConnectionTestPeriod(dataSourceProperties.getIdleConnectionTestPeriod());
dataSource.setAcquireRetryDelay(dataSourceProperties.getAcquireRetryDelay());
dataSource.setAcquireRetryAttempts(dataSourceProperties.getAcquireRetryAttempts());
dataSource.setBreakAfterAcquireFailure(dataSourceProperties.isBreakAfterAcquireFailure());
dataSource.setTestConnectionOnCheckout(dataSourceProperties.isTestConnectionOnCheckout());
log.info("Initialization of c3p0 connection pool completed");
return dataSource;
}
}
复制代码
@ConfigurationProperties(prefix = "c3p0")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class C3P0DataSourceProperties {
/**
* 默认设置如下配置项并提供默认值
*/
private String driver = "com.mysql.jdbc.Driver";
private String jdbcUrl = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong";
private String username = "root";
private String password = "root";
private int minPoolSize = 2;
private int initialPoolSize = 10;
private int maxPoolSize = 50;
private int acquireIncrement = 5;
private int maxIdleTime = 1800000;
private int maxStatements = 1000;
private int idleConnectionTestPeriod = 60;
private int acquireRetryAttempts = 30;
private int acquireRetryDelay = 1000;
private boolean breakAfterAcquireFailure = false;
private boolean testConnectionOnCheckout = false;
}
复制代码
请在resource下的META-INF创建 spring.factories
#启动自动配置 org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xianglei.config.C3p0DataSourceAutoConfiguration 复制代码
spring.factories解释:
剩下的大家可以自己测试一下我就不再赘述了。
Spring官方强烈建议非官方Starter命名应遵循{name}-spring-boot-starter的格式。总结一下,spring-boot是通过spring-boot-autoconfigure体现了"约定优于配置"的原则,而spring-boot-autoconfigure主要用到了spring.factories和几个常用的条件注解来实现自动配置。
手写starter一共四步。
引依赖:主要是 spring-boot-autoconfigure
写自动配置类
配置项注入类:这一步大家也可以用@Value(“xxx:xxx”)注入参数的方式实现。看个人喜好。
添加配置文件
各位小伙伴,更多代码细节请光顾我的 GitHub仓库 ,如有疑问欢迎大家提出 Issue ,想成为 Collaborator 大家可以私信我~ :mailbox: