Hatred's Log Place

DON'T PANIC!

Mar 21, 2012 - 3 minute read - programming

Quartz Scheduler

Потребовалось использовать в одном проекте данный планировщик ( http://quartz-scheduler.org/). Запускается на ура в виде бина в JBoss… Но вот тут с размаху врезался лбом в косяк:

  1. планировщик регистрируется в JNDI
  2. я успешно получаю инстанс планировщика
  3. добавляю свои задачу со своим воркером

И кряк вам с хреном, а не профит: ClassNotFoundException…

“Централизованный” инстанс планировщика не видит ваших классов-реализаций интерфейса Job. При ближайшем рассмотрении оно и понятно. При дальнейшем - не понятно зачем тогда нужно запускать Quartz как сервис, может кто объяснит?

В результате сделал, что бы внутри приложения запускался свой инстанс Quartz.

Для сих нужд уже есть два класса:

  • QuartzInitializerListener
  • QuartzInitializerServlet

Как их использовать - есть в документации на API. Я немного расширил QuartzInitializerListener, сделав регистрацию полученного инстанса планировщика в JNDI, выглядит примерно так:

package net.homelinux.hatred.quartz;

import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;

import org.jboss.naming.NonSerializableFactory;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Обёртка для стандартного QuartzInitializerListener, что бы зарегистрировать фабрику в том числе
 * за пределами Сервлет-контекстов
 * <p/>
 * Для подробностей смотреть параметры конфигурации org.quartz.ee.servlet.QuartzInitializerListener:
 * http://quartz-scheduler.org/api/2.1.0/org/quartz/ee/servlet/QuartzInitializerListener.html
 *
 * @author Alexander 'hatred' Drozdov
 *         <p/>
 *         Date: 19.03.12
 *         Time: 11:08
 */
public class QuartzInitializerListener extends org.quartz.ee.servlet.QuartzInitializerListener
{
    // Logger
    private static final Logger log = LoggerFactory.getLogger(QuartzInitializerListener.class);


    // TODO: хранить где-то в другом месте
    public static final String JNDI_NAME = "MyAppQuartzScheduler";


    @Override
    public void contextInitialized(ServletContextEvent sce)
    {
        super.contextInitialized(sce);

        ServletContext ctx = sce.getServletContext();
        StdSchedulerFactory factory = (StdSchedulerFactory) ctx
                .getAttribute(QuartzInitializerListener.QUARTZ_FACTORY_KEY);

        try
        {
            rebind(factory, JNDI_NAME);
        }
        catch (NamingException e)
        {
            e.printStackTrace();
        }
        catch (SchedulerException e)
        {
            e.printStackTrace();
        }

    }


    @Override
    public void contextDestroyed(ServletContextEvent sce)
    {
        unbind(JNDI_NAME);
        super.contextDestroyed(sce);
    }


    ///
    //
    // Биндимся к JNDI
    //
    ///
    private void rebind(StdSchedulerFactory factory, String name)
            throws NamingException, SchedulerException
    {
        InitialContext rootCtx = null;
        try
        {
            rootCtx = new InitialContext();
            Name fullName = rootCtx.getNameParser("").parse(name);
            Scheduler scheduler = factory.getScheduler();
            NonSerializableFactory.rebind(fullName, scheduler, true);
        }
        finally
        {
            if (rootCtx != null)
            {
                try
                {
                    rootCtx.close();
                }
                catch (NamingException ignore)
                {
                }
            }
        }
    }


    private void unbind(String name)
    {
        InitialContext rootCtx = null;
        try
        {
            rootCtx = new InitialContext();
            rootCtx.unbind(name);
            NonSerializableFactory.unbind(name);
        }
        catch (NamingException e)
        {
            log.warn("Failed to unbind scheduler with jndiName: " + name, e);
        }
        finally
        {
            if (rootCtx != null)
            {
                try
                {
                    rootCtx.close();
                }
                catch (NamingException ignore)
                {
                }
            }
        }
    }
}

после чего получить доступ к фабрике можно по JNDI имени “MyAppQuartzScheduler”.

Затем настройки самого Quatrz.

В случае JBoss 6.0, я их сохранил в директории ./conf:

# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#

#org.quartz.scheduler.classLoadHelper.class = org.quartz.simpl.CascadingClassLoadHelper
#org.quartz.scheduler.classLoadHelper.class = org.quartz.simpl.LoadingLoaderClassLoadHelper

org.quartz.scheduler.instanceName = PodryadQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.xaTransacted = false
org.quartz.scheduler.threadsInheritContextClassLoaderOfInitializer = true

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 4
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT
org.quartz.jobStore.driverDelegateClass =  org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = QUARTZ
org.quartz.jobStore.nonManagedTXDataSource = QUARTZ_NO_TX
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.dataSource.QUARTZ.jndiURL = java:jdbc/quartz_scheduler
org.quartz.dataSource.QUARTZ_NO_TX.jndiURL = java:jdbc/quartz_scheduler

#org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#org.quartz.jobStore.driverDelegateClass =  org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#org.quartz.jobStore.dataSource = QUARTZ
#org.quartz.jobStore.tablePrefix = QRTZ_
#org.quartz.dataSource.QUARTZ.jndiURL = java:jdbc/quartz_scheduler

Отдельно стоит отметить и описание Datasource для подключения к базе.

Если вы его объявите как ’local-tx-datasource’ получите небольшой обломс, поэтому нужно объявлять как ‘xa-datasource’, примерно следующим образом:

<?xml version="1.0" encoding="UTF-8"?>

<!-- See http://www.jboss.org/community/wiki/Multiple1PC for information about local-tx-datasource -->
<!-- $Id: mysql-ds.xml 97536 2009-12-08 14:05:07Z jesper.pedersen $ -->
<!--  Datasource config for MySQL using 3.0.9 available from:
http://www.mysql.com/downloads/api-jdbc-stable.html
-->

<datasources>

...

  <!-- Quartz datasource, must be 'xa-datasource', but no 'local-tx-datasource' -->
  <xa-datasource>
    <jndi-name>jdbc/quartz_scheduler</jndi-name>

    <xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
    <xa-datasource-property name="URL">jdbc:mysql://127.0.0.1:3306/jboss_quartz?useEncoding=true&amp;characterEncoding=UTF-8</xa-datasource-property>
    <xa-datasource-property name="User">DB_USER</xa-datasource-property>
    <xa-datasource-property name="Password">DB_PASSWORD</xa-datasource-property>

    <min-pool-size>5</min-pool-size>
    <max-pool-size>100</max-pool-size>
    <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>
    <valid-connection-checker-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLValidConnectionChecker</valid-connection-checker-class-name>
    <new-connection-sql>SELECT 1</new-connection-sql>
    <metadata>
       <type-mapping>mySQL</type-mapping>
    </metadata>
  </xa-datasource>

...
</datasources>

PS если кто объяснит, для чего всё же нужен Quartz в виде сервиса в JBoss, буду премного благодарен. PPS ещё, ориентировочно с версии Quartz 2.0, несколько инстансев оного могут использовать одну базу, нужно только, что бы имена инстансев различались, дабы не было конфликтов.