Статьи

Конфигурирование кварца с JDBCJobStore весной

Я начинаю небольшую серию о внутренностях, советах и ​​хитростях планировщика Quartz , это глава 0 — как настроить постоянное хранилище заданий. В Quartz у вас есть выбор между хранением заданий и триггеров в памяти и в базе данных отношений ( Terracotta — недавнее дополнение к миксу). Я бы сказал, что в 90% случаев, когда вы используете RAMJobStore с Quartz, вам совсем не нужен Quartz. Очевидно, что этот внутренний сервер хранения является временным, и все ваши ожидающие задания и триггеры теряются между перезапусками. Если вы согласны с этим, доступны гораздо более простые и легкие решения, включая ScheduledExecutorService, встроенный в JDK, и @Scheduled (cron = ”* / 5 * * * * MON-FRI”) в Spring. Можете ли вы оправдать использование дополнительных 0,5 МБ JAR в этом сценарии?

Это резко меняется, когда вам нужны кластеризация, отработка отказа, балансировка нагрузки и несколько других модных слов. Для этого есть несколько вариантов использования:

  • один сервер не может обработать требуемое количество одновременных, длительных заданий, и выполнение должно быть разделено на несколько машин, но каждая задача должна выполняться точно
  • мы не можем позволить себе запускать задания слишком поздно — если один сервер не работает, другой должен выполнить его вовремя
  • … или менее строго — задание должно быть выполнено в конце концов — даже если один и тот же сервер не работает из-за обслуживания, отложенные задания должны быть запущены как можно скорее после перезапуска

Во всех вышеперечисленных случаях нам нужно какое-то нестационарное глобальное хранилище, чтобы отслеживать, какие задания были выполнены, чтобы они выполнялись ровно на одной машине. Реляционная база данных в качестве разделяемой памяти хорошо работает в этом сценарии.

Поэтому, если вы считаете, что вам нужно запланировать работу и выполнить некоторые из вышеуказанных требований, продолжайте читать. Я покажу вам, как настроить 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 и соседстве .