0%

自定义spring-boot-starter

自定义Starter:myQuartz-spring-boot-starter

模仿学习Spring Boot starter写一个关于Quartz的自动配置的依赖。

1. 自定义Starter的模块结构

模块结构.png

2. Maven 依赖配置

自定义starter命名方式:

  • 官方 spring-boot-starter-模块名
  • 非官方(如我们自己编写的) 模块名-spring-boot-starter

spring-boot-configuration-processor是2.x必须引入的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.littlefxc.examples</groupId>
<artifactId>myQuartz-spring-boot-starter</artifactId>
<version>1.0-snapshot</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.18.RELEASE</version>
<relativePath />
</parent>

<properties>
<quartz.version>2.3.0</quartz.version>
</properties>

<dependencies>
<!-- starter的基本配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- quartz的基本配置 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
<exclusions>
<exclusion>
<artifactId>c3p0</artifactId>
<groupId>com.mchange</groupId>
</exclusion>
<exclusion>
<artifactId>HikariCP-java6</artifactId>
<groupId>com.zaxxer</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
</project>

3. 创建自动配置类QuartzAutoConfiguration

构造函数隐式注入
在4.3之前,如果你构造函数中要依赖另外一个bean,你必须显示依赖@Autowired, 例如:

1
2
3
4
5
6
7
8
@Service
public class FooService {
private final FooRepository repository;
@Autowired
public FooService(FooRepository repository) {
this.repository = repository
}
}

相当常见的用例但是如果你忘记构造函数上的@Autowired注释,容器将抛出一个寻找默认构造函数的异常,除非你在bean定义设置中明确指出autowire模式’constructor’。

因此,从4.3开始,您不再需要在这样的单构造函数场景中指定显式注入注释。对于那些根本不带任何容器注释的类来说,这是特别优雅的,FooService会从beanFactory中查找FooRepository。

同样的,@Configuration类在4.3之前不支持构造函数注入。

为什么要使用ObjectProvider?

上文讲过构造函数隐式注入,但其有个缺点那就是强依赖。
Spring Framework 4.3引入了ObjectProvider,它是现有ObjectFactory接口的扩展,具有方便的签名,例如getIfAvailable和getIfUnique,只有在它实际存在时才检索bean(可选支持)或者如果可以确定单个候选者(特别是:主要候选者)在多个匹配的bean的情况下)。它的Spring核心源码见org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency()

** 用到的几个和Starter相关的注解:

  • @ConditionalOnClass,当classpath下发现该类的情况下进行自动配置。
  • @ConditionalOnMissingBean,当Spring Context中不存在该Bean时。
  • @EnableConfigurationProperties(QuartzProperties.class),使@ConfigurationProperties注解生效。
  • @ConfigurationProperties,主要用来把properties配置文件转化为bean。
  • @AutoConfigureAfter,自动注入该类在什么类加载之后。

自定义Quartz的调度器工厂Bean的自动配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@Configuration
@ConditionalOnClass({Scheduler.class, SchedulerFactoryBean.class, PlatformTransactionManager.class})
@EnableConfigurationProperties(QuartzProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class QuartzAutoConfiguration {

private final static Logger log = LoggerFactory.getLogger(QuartzAutoConfiguration.class);

private final List<SchedulerFactoryBeanCustomizer> customizers;

private final QuartzProperties properties;

private final JobDetail[] jobDetails;

private final Map<String, Calendar> calendars;

private final Trigger[] triggers;

private final ApplicationContext applicationContext;

private final DataSource dataSource;

private final PlatformTransactionManager transactionManager;

public QuartzAutoConfiguration(QuartzProperties properties,
ObjectProvider<List<SchedulerFactoryBeanCustomizer>> customizers,
ObjectProvider<JobDetail[]> jobDetails,
ObjectProvider<Map<String, Calendar>> calendars,
ObjectProvider<Trigger[]> triggers,
ApplicationContext applicationContext,
ObjectProvider<DataSource> dataSource,
ObjectProvider<PlatformTransactionManager> transactionManager) {
this.properties = properties;
this.jobDetails = jobDetails.getIfAvailable();
this.calendars = calendars.getIfAvailable();
this.triggers = triggers.getIfAvailable();
this.applicationContext = applicationContext;
this.dataSource = dataSource.getIfAvailable();
this.transactionManager = transactionManager.getIfAvailable();
this.customizers = customizers.getIfAvailable();
}

/**
* 如果没有调度器,就创建
*
* @return
*/
@Bean
@ConditionalOnMissingBean
public SchedulerFactoryBean schedulerFactoryBean() {
log.info("Init SchedulerFactoryBean");
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobFactory(new AutoSchedulerJobFactory(this.applicationContext.getAutowireCapableBeanFactory()));
if (!this.properties.getProperties().isEmpty()) {
schedulerFactoryBean.setQuartzProperties(asProperties(this.properties.getProperties()));
}
if (this.jobDetails != null && this.jobDetails.length > 0) {
schedulerFactoryBean.setJobDetails(this.jobDetails);
}
if (this.calendars != null && !this.calendars.isEmpty()) {
schedulerFactoryBean.setCalendars(this.calendars);
}
if (this.triggers != null && this.triggers.length > 0) {
schedulerFactoryBean.setTriggers(this.triggers);
}
// todo quartz数据源初始化
if (properties.getJobStoreType() == JobStoreType.JDBC) {
if (dataSource != null) {
schedulerFactoryBean.setDataSource(dataSource);
}
if (transactionManager != null) {
schedulerFactoryBean.setTransactionManager(transactionManager);
}
}
customize(schedulerFactoryBean);
return schedulerFactoryBean;
}

/**
* 自定义调度器
*
* @param schedulerFactoryBean
*/
private void customize(SchedulerFactoryBean schedulerFactoryBean) {
if (this.customizers != null) {
for (SchedulerFactoryBeanCustomizer customizer : this.customizers) {
customizer.customize(schedulerFactoryBean);
}
}
}

private Properties asProperties(Map<String, String> source) {
Properties properties = new Properties();
properties.putAll(source);
return properties;
}

}

4. resources/META-INF/spring.factories

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.littlefxc.examples.spring.boot.autoconfigure.QuartzAutoConfiguration

5. 其它

QuartzProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ConfigurationProperties("spring.quartz")
public class QuartzProperties {

/**
* Quartz job store type.
*/
private JobStoreType jobStoreType = JobStoreType.MEMORY;

/**
* Additional Quartz Scheduler properties.
*/
private final Map<String, String> properties = new HashMap<>();

public JobStoreType getJobStoreType() {
return this.jobStoreType;
}

public void setJobStoreType(JobStoreType jobStoreType) {
this.jobStoreType = jobStoreType;
}

public Map<String, String> getProperties() {
return this.properties;
}
}

JobStoreType

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum JobStoreType {

/**
* Store jobs in memory.
*/
MEMORY,

/**
* Store jobs in the database.
*/
JDBC

}

AutoSchedulerJobFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 模仿了:{@link org.springframework.boot.autoconfigure.quartz.AutowireCapableBeanJobFactory}
*
* @author fengxuechao
* @date 12/19/2018
* @see <a href="http://blog.btmatthews.com/?p=40#comment-33797">注入Spring上下文(applicationContext)
*/
public class AutoSchedulerJobFactory extends SpringBeanJobFactory {

private AutowireCapableBeanFactory beanFactory;

AutoSchedulerJobFactory(AutowireCapableBeanFactory factory) {
beanFactory = factory;
}

@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
this.beanFactory.initializeBean(job, null);
return job;
}
}

SchedulerFactoryBeanCustomizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 回调接口,可以由希望在完全初始化之前自定义Quartz SchedulerFactoryBean的bean实现,特别是调整其配置。
* @author fengxuechao
*/
@FunctionalInterface
public interface SchedulerFactoryBeanCustomizer {

/**
* 自定义{@link SchedulerFactoryBean}.
* @param schedulerFactoryBean the scheduler to customize
*/
void customize(SchedulerFactoryBean schedulerFactoryBean);

}

6. 测试

6.1 项目结构

新建项目,结构如下图所示:

test-autoconfigure项目结构.png

6.2 Maven 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.littlefxc.examples</groupId>
<artifactId>test-autoconfigure</artifactId>
<version>1.0-snapshot</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--<version>2.0.4.RELEASE</version>-->
<version>1.5.18.RELEASE</version>
<relativePath />
</parent>

<properties>
<quartz.version>2.3.0</quartz.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.littlefxc.examples</groupId>
<artifactId>myQuartz-spring-boot-starter</artifactId>
<version>1.0-snapshot</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>${quartz.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>
</project>

6.3 application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
logging.level.root=warn
logging.level.com.littlefxc.examples=debug

# DataSource Config
spring.datasource.url=jdbc:mysql://localhost:3306/learn-quartz?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.filters=slf4j,wall
spring.datasource.druid.initial-size=1
spring.datasource.druid.min-idle=1
spring.datasource.druid.max-active=8
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20

# Quartz Config
spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.scheduler.instanceName=schedulerFactoryBean
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.scheduler.instanceIdGenerator.class=com.littlefxc.examples.CustomQuartzInstanceIdGenerator
spring.quartz.properties.org.quartz.threadPool.threadCount=20
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.useProperties=true
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000
spring.quartz.properties.org.quartz.jobStore.tablePrefix=qrtz_
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.plugin.shutdownHook.class=org.quartz.plugins.management.ShutdownHookPlugin
spring.quartz.properties.org.quartz.plugin.shutdownHook.cleanShutdown=TRUE

6.4 代码

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Slf4j
@SpringBootApplication
public class TestAutoConfigure {

public static void main(String[] args) {
SpringApplication.run(TestAutoConfigure.class, args);
}

@Bean
public SchedulerFactoryBeanCustomizer dataSourceCustomizer() {
return (schedulerFactoryBean) -> {
schedulerFactoryBean.setOverwriteExistingJobs(false);
schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true);
schedulerFactoryBean.setStartupDelay(10);
};
}

/**
* JobDetailFactoryBean
*
* @return
*/
@Bean
public JobDetailFactoryBean jobDetailFactoryBean() {
JobDetailFactoryBean bean = new JobDetailFactoryBean();
bean.setName("job-1");
bean.setGroup("job-group-1");
bean.setJobClass(MyJob.class);
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("hello", "world");
bean.setJobDataMap(jobDataMap);
bean.setDurability(true);
return bean;
}

/**
* CronTriggerFactoryBean
*
* @param jobDetail
* @return
*/
@Bean
public CronTriggerFactoryBean cronTrigger(JobDetail jobDetail) {
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
bean.setName("cron-1");
bean.setGroup("cron-group-1");
bean.setCronExpression("0/5 * * * * ?");
bean.setJobDetail(jobDetail);
return bean;
}

/**
* Job
*/
@Component
public static class MyJob extends QuartzJobBean {

@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDetail jobDetail = jobExecutionContext.getJobDetail();
String jobName = jobDetail.getKey().getName();
String jobGroup = jobDetail.getKey().getGroup();
String jobDataMapHello = (String) jobDetail.getJobDataMap().get("hello");
log.info("job.name = {}, job.group = {}, job.dataMap.hello = {}", jobName, jobGroup, jobDataMapHello);
}
}
}

CustomQuartzInstanceIdGenerator

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomQuartzInstanceIdGenerator implements InstanceIdGenerator {

@Override
public String generateInstanceId() throws SchedulerException {
try {
return UUID.randomUUID().toString().replaceAll("-", "");
} catch (Exception ex) {
throw new SchedulerException("Couldn't generate UUID!", ex);
}
}

}