Quartz JobStore: JobPersistenceException: no se pudo recuperar el disparador: ClassNotFoundException

¡Espero que uno de ustedes, gente encantadora, pueda ayudarme con esto, ya que he pasado varias horas infructuosas tratando de hacer que todo funcione bien!

Rastreé el problema hasta Classloading y pude ver que cuando Quartz intenta deserializar jobDetail de un jobStore (jobStoreCMT), el Classloader utilizado no contiene ninguna de mis clases de aplicaciones, y solo las bibliotecas definidas en el Carpeta lib de EAR.

Entonces... obviamente estoy usando un servidor de aplicaciones, y en este caso probé contra Glassfish 3.1.1/3.1.2

probado contra Quartz 1.8.6/2.1.5 usando Spring 3.1.0.RELEASE

Configuración de primavera/cuarzo:

<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="overwriteExistingJobs" value="true" />

    <property name="triggers">
        <list>
            <ref bean="notificationEmailsSimpleTrigger" />
        </list>
    </property>

    <property name="quartzProperties">
        <props>
            <prop key="org.quartz.scheduler.instanceName">QuartzScheduler</prop>
            <prop key="org.quartz.scheduler.instanceId">AUTO</prop>

            <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>
            <prop key="org.quartz.threadPool.threadCount">25</prop>
            <prop key="org.quartz.threadPool.threadPriority">5</prop>
            <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreCMT</prop>
            <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop>
            <prop key="org.quartz.jobStore.misfireThreshold">60000</prop>
            <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop>
            <!-- <prop key="org.quartz.jobStore.isClustered">true</prop> -->
            <!-- <prop key="org.quartz.jobStore.clusterCheckinInterval">20000</prop> -->

            <prop key="org.quartz.scheduler.classLoadHelper.class">org.quartz.simpl.CascadingClassLoadHelper</prop>
            <prop key="org.quartz.scheduler.threadsInheritContextClassLoaderOfInitializer">true</prop>
            <prop key="org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread">true</prop>

            <prop key="org.quartz.scheduler.skipUpdateCheck">true</prop>
        </props>
    </property>
</bean>

y la referencia del disparador correspondiente:

<bean id="notificationEmailsSimpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
    <property name="jobDetail" ref="notificationJobDetail" />
    <property name="repeatInterval" value="60000" />
</bean>

<bean id="notificationJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
    <property name="jobClass" value="com.mcboom.social.notifications.NotificationQuartzJobBean" />
</bean>

Entonces, el problema que tengo es este: cualquier combinación de lo siguiente no parece afectar el cargador de clases que se está utilizando.

    <prop key="org.quartz.scheduler.classLoadHelper.class">org.quartz.simpl.CascadingClassLoadHelper</prop>
    <prop key="org.quartz.scheduler.threadsInheritContextClassLoaderOfInitializer">true</prop>
    <prop key="org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread">true</prop>

O, más específicamente, no ayuda al intentar recuperar un disparador persistente anteriormente, lo que da como resultado el siguiente seguimiento:

INFO: ERROR - ErrorLogger.schedulerError(schedulerFactoryBean_QuartzSchedulerThread)(2358) | An error occured while scanning for the next trigger to fire.
org.quartz.JobPersistenceException: Couldn't acquire next trigger: Couldn't retrieve trigger: com.mcboom.social.notifications.NotificationQuartzJobBean [See nested exception: org.quartz.JobPersistenceException: Couldn't retrieve trigger: com.mcboom.social.notifications.NotificationQuartzJobBean [See nested exception: java.lang.ClassNotFoundException: com.mcboom.social.notifications.NotificationQuartzJobBean]]
at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTrigger(JobStoreSupport.java:2814)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$36.execute(JobStoreSupport.java:2757)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3788)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTrigger(JobStoreSupport.java:2753)
at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:263)
Caused by: org.quartz.JobPersistenceException: Couldn't retrieve trigger: com.mcboom.social.notifications.NotificationQuartzJobBean [See nested exception: java.lang.ClassNotFoundException: com.mcboom.social.notifications.NotificationQuartzJobBean]
at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveTrigger(JobStoreSupport.java:1596)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveTrigger(JobStoreSupport.java:1572)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTrigger(JobStoreSupport.java:2792)
... 4 more

Puedo ver el org.quartz.simpl.CascadingClassLoadHelper siendo usado en carga, y seleccionando correctamente el cargador de clases correcto.

El problema es que cuando el CuarzoProgramadorHilo intenta recuperar un activador que utiliza JobStoreSupport.retrieveTrigger(), que a su vez recurre a ObjectInputsStream.resolveClass(), y la siguiente línea de código:

Class.forName(name, false, latestUserDefinedLoader())

Dónde último cargador definido por el usuario () siempre devuelve el cargador de clases incorrecto... lo que da como resultado la ClassNotFoundException y me deja bastante desconcertado.

Debo señalar que último cargador definido por el usuario () es un método nativo de ObjectInputsStream y estoy usando jdk 1.6 en OSX.

¿Alguien de la comunidad de Quartz/Spring o, más probablemente, de Glassfish, puede arrojar algo de luz sobre esto? Me estoy tirando de los pelos en este momento.

Gracias Steve.

preguntado el 22 de mayo de 12 a las 15:05

2 Respuestas

Si a alguien le interesó la solución que se me ocurrió, aunque no lo ideal, fue extender el valor predeterminado StdJDBCDelegate, en mi caso para MySQL, donde pude anular de forma anónima ObjectInputStream.resolveClass() muy parecido al de abajo.

/**
 * <p>
 * This method should be overridden by any delegate subclasses that need
 * special handling for BLOBs. The default implementation uses standard JDBC
 * <code>java.sql.Blob</code> operations.
 * </p>
 * 
 * <p>
 * This implementation overcomes the incorrect classloader being used in
 * ObjectInputStream, overriding it with the current threads classloader.
 * </p>
 * 
 * @param rs
 *            the result set, already queued to the correct row
 * @param colName
 *            the column name for the BLOB
 * @return the deserialized Object from the ResultSet BLOB
 * @throws ClassNotFoundException
 *             if a class found during deserialization cannot be found
 * @throws IOException
 *             if deserialization causes an error
 */
@Override
protected Object getObjectFromBlob(ResultSet rs, String colName) throws ClassNotFoundException, IOException, SQLException {
    Object obj = null;

    Blob blobLocator = rs.getBlob(colName);
    if (blobLocator != null && blobLocator.length() != 0) {
        InputStream binaryInput = blobLocator.getBinaryStream();

        if (null != binaryInput) {
            if (binaryInput instanceof ByteArrayInputStream && ((ByteArrayInputStream) binaryInput).available() == 0) {
                // do nothing
            } else {
                ObjectInputStream in = new ObjectInputStream(binaryInput) {

                    @Override
                    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
                        String name = desc.getName();
                        try {
                            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
                        } catch (ClassNotFoundException ex) {
                            return super.resolveClass(desc);
                        }
                    }
                };

                try {
                    obj = in.readObject();
                } finally {
                    in.close();
                }
            }
        }

    }
    return obj;
}   

Usando el cargador de clases de subprocesos actual, recurriendo al manejo predeterminado si no tiene éxito.

Si alguien tiene una mejor solución o incluso una explicación de por qué ocurrió el problema en primer lugar, estaría más que interesado en escucharla.

S.

contestado el 28 de mayo de 12 a las 17:05

Una solución más elegante fue volver a empaquetar mi EAR, empaquetando todas las clases que no son EJB en core.jar lib y colocándolo en la carpeta lib de EAR, para compartir el mismo cargador de clases que todas las demás aplicaciones. Se resolvió el problema, pero conllevó la sobrecarga no deseada de una cantidad significativa de refactorización. - stephen murphy

Utiliza org.springframework.scheduling.quartz.JobDetailBean org.springframework.scheduling.quartz.JobDetailFactoryBean

contestado el 22 de mayo de 12 a las 18:05

org.springframework.scheduling.quartz.JobDetailFactoryBean bean es un reemplazo directo para el org.springframework.scheduling.quartz.JobDetailBean para admitir Quartx 2.x, así que no hay ayuda, me temo. Gracias por la sugerencia. - stephen murphy

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.