Это резко меняется, когда вам нужны кластеризация, отработка отказа, балансировка нагрузки и несколько других модных слов. Для этого есть несколько вариантов использования:
- один сервер не может обработать требуемое количество одновременных, длительных заданий, и выполнение должно быть разделено на несколько машин, но каждая задача должна выполняться точно
- мы не можем позволить себе запускать задания слишком поздно — если один сервер не работает, другой должен выполнить его вовремя
- … или менее строго — задание должно быть выполнено в конце концов — даже если один и тот же сервер не работает из-за обслуживания, отложенные задания должны быть запущены как можно скорее после перезапуска
Во всех вышеперечисленных случаях нам нужно какое-то нестационарное глобальное хранилище, чтобы отслеживать, какие задания были выполнены, чтобы они выполнялись ровно на одной машине. Реляционная база данных в качестве разделяемой памяти хорошо работает в этом сценарии.
Поэтому, если вы считаете, что вам нужно запланировать работу и выполнить некоторые из вышеуказанных требований, продолжайте читать. Я покажу вам, как настроить Quartz с помощью Spring и полностью интегрировать их. Прежде всего нам нужен источник данных:
01
02
03
04
05
06
07
08
09
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
|
import org.apache.commons.dbcp.BasicDataSource import com.googlecode.flyway.core.Flyway import org.jdbcdslog.DataSourceProxy import org.springframework.jdbc.datasource.{DataSourceTransactionManager, LazyConnectionDataSourceProxy} import org.h2.Driver @Configuration @EnableTransactionManagement class Persistence { @Bean def transactionManager() = new DataSourceTransactionManager(dataSource()) @Bean @Primary def dataSource() = { val proxy = new DataSourceProxy() proxy.setTargetDSDirect(dbcpDataSource()) new LazyConnectionDataSourceProxy(proxy) } @Bean (destroyMethod = "close" ) def dbcpDataSource() = { val dataSource = new BasicDataSource dataSource.setDriverClassName(classOf[Driver].getName) dataSource.setUrl( "jdbc:h2:mem:quartz-demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MVCC=TRUE" ) dataSource.setUsername( "sa" ) dataSource.setPassword( "" ) dataSource.setMaxActive( 20 ) dataSource.setMaxIdle( 20 ) dataSource.setMaxWait( 10000 ) dataSource.setInitialSize( 5 ) dataSource.setValidationQuery( "SELECT 1" ) dataSource } } |
Как вы уже догадались, для работы с Quartz нужны таблицы базы данных. Он не создает их автоматически, но предоставляются сценарии SQL для нескольких баз данных, включая H2, который, как вы можете видеть, я использую. Я думаю, что Flyway — это самый простой способ запуска сценариев базы данных при запуске:
1
2
3
4
5
6
|
@Bean (initMethod = "migrate" ) def flyway() = { val fly = new Flyway() fly.setDataSource(dataSource()) fly } |
Кстати, если вы не заметили: нет, в нашем примере приложения нет XML, и да, мы используем Spring.
Давайте перейдем к Кварцу:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Configuration class Scheduling { @Resource val persistence: Persistence = null @Bean @DependsOn (Array( "flyway" )) def schedulerFactory() = { val schedulerFactoryBean = new SchedulerFactoryBean() schedulerFactoryBean.setDataSource(persistence.dataSource()) schedulerFactoryBean.setTransactionManager(persistence.transactionManager()) schedulerFactoryBean.setConfigLocation( new ClassPathResource( "quartz.properties" )) schedulerFactoryBean.setJobFactory(jobFactory()) schedulerFactoryBean.setApplicationContextSchedulerContextKey( "applicationContext" ) schedulerFactoryBean.setSchedulerContextAsMap(Map().asJava) schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown( true ) schedulerFactoryBean } @Bean def jobFactory() = new SpringBeanJobFactory } |
Приятно знать, что для удобства вы можете внедрить экземпляр аннотированных классов @Configuration в другой такой класс. Кроме этого — ничего особенного. Обратите внимание, что нам нужен @DependsOn (Array («flyway»)) на фабрике планировщиков Quartz — иначе планировщик может запуститься до того, как Flyway запустит скрипт миграции с таблицами базы данных Quartz, что вызовет неприятные ошибки при запуске. Основными битами являются SpringBeanJobFactory и schedulerContextAsMap. Специальная фабрика делает Spring ответственным за создание экземпляров Job. К сожалению, эта фабрика довольно ограничена, что мы вскоре увидим в следующем примере. Для начала нам понадобится Spring Bean и Quartz:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@Service class Printer extends Logging { def print(msg: String) { logger.info(msg) } } class PrintMessageJob extends Job with Logging { @BeanProperty var printer: Printer = _ @BeanProperty var msg = "" def execute(context: JobExecutionContext) { printer print msg } } |
Первый неожиданный ввод — @BeanProperty вместо @Autowired или @Resource. Оказывается, что Job на самом деле не является компонентом Spring, хотя Spring создает его экземпляр. Вместо этого Spring обнаруживает необходимые зависимости, используя доступные установщики. Итак, откуда берется строка сообщения? Продолжай идти:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
val trigger = newTrigger(). withIdentity( "Every-few-seconds" , "Demo" ). withSchedule( simpleSchedule(). withIntervalInSeconds( 4 ). repeatForever() ). build() val job = newJob(classOf[PrintMessageJob]). withIdentity( "Print-message" , "Demo" ). usingJobData( "msg" , "Hello, world!" ). build() scheduler.scheduleJob(job, trigger) |
Quartz 2.0 поставляется с хорошим внутренним DSL для создания рабочих мест и запуска в удобочитаемом виде. Как вы видите, я передаю дополнительный «Привет, мир!» параметр для работы. Этот параметр хранится в так называемых JobData в базе данных для каждого задания или для триггера. Он будет предоставлен заданию при его выполнении. Таким образом, вы можете параметризовать свою работу. Однако при выполнении наша работа выдает NullPointerException… Видимо, ссылка на принтер не была установлена и молча игнорировалась. Оказывается, Spring не просто просматривает все компоненты, доступные в ApplicationContext. SpringBeanJobFactory просматривает только JobData Джобса и Триггеров и так называемый контекст планировщика (уже упоминалось). Если вы хотите внедрить какой-либо bean-компонент Spring в Job, вы должны явно поместить ссылку на этот bean-компонент в schedulerContext:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@Configuration class Scheduling { @Resource val printer: Printer = null @Bean def schedulerFactory() = { val schedulerFactoryBean = new SchedulerFactoryBean() //... schedulerFactoryBean.setSchedulerContextAsMap(schedulerContextMap().asJava) //... schedulerFactoryBean } def schedulerContextMap() = Map( "printer" -> printer ) } |
К сожалению, каждый bean-компонент Spring, который вы хотите внедрить в задание, должен быть явно указан в schedulerContextMap. Еще хуже, если вы забудете об этом, Quartz будет молча регистрировать NPE во время выполнения. В будущем мы напишем более надежную работу завода. Но для начала у нас есть работающее приложение Spring + Quartz, готовое для дальнейших экспериментов, источники как всегда доступны под моей учетной записью GitHub.
Вы можете спросить себя, не можем ли мы просто использовать MethodInvokingJobDetailFactoryBean ? Ну, прежде всего потому, что он не работает с постоянными магазинами вакансий. Во-вторых, поскольку он не может передать JobData в Job, мы больше не можем различать разные запуски заданий. Например, наше сообщение о печати задания должно всегда печатать одно и то же сообщение, жестко закодированное в классе.
Кстати, если кто-нибудь спросит вас: сколько классов нужно разработчику Java для предприятия, чтобы напечатать «Hello world!» Вы можете с гордостью ответить: 4 класса, 30 JAR, занимающих 20 МБ пространства, и реляционная база данных с 10+ таблицами. Серьезно, это вывод нашей статьи здесь …
Ссылка: Конфигурирование Quartz с JDBCJobStore весной от нашего партнера JCG Томаша Нуркевича в блоге о Java и соседстве .