Search code examples
javadependency-injectionquartz-schedulerhk2

HK2 factory for Quartz jobs, not destroying service after execution


I want to use Quartz Scheduler in my server application that uses HK2 for dependency injection. In order for Quartz jobs to have access to DI, they need to be DI-managed themselves. As a result, I wrote a super simple HK2-aware job factory and registered it with the scheduler.

It works fine with instantiation of services, observing the requested @Singleton or @PerLookup scope. However, it's failing to destroy() non-singleton services (= jobs) after they are finished.

Question: how do I get HK2 to manage jobs properly, including tearing them down again?

Do I need to go down the path of creating the service via serviceLocator.getServiceHandle() and later manually destroy the service, maybe from a JobListener (but how get the ServiceHandle to it)?

Hk2JobFactory.java

@Service
public class Hk2JobFactory implements JobFactory {
    private final Logger log = LoggerFactory.getLogger(getClass());

    @Inject
    ServiceLocator serviceLocator;

    @Override
    public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {
        JobDetail jobDetail = bundle.getJobDetail();
        Class<? extends Job> jobClass = jobDetail.getJobClass();
        try {
            log.debug("Producing instance of Job '" + jobDetail.getKey() + "', class=" + jobClass.getName());

            Job job = serviceLocator.getService(jobClass);
            if (job == null) {
                log.debug("Unable to instantiate job via ServiceLocator, returning unmanaged instance.");
                return jobClass.newInstance();
            }
            return job;

        } catch (Exception e) {
            SchedulerException se = new SchedulerException(
                    "Problem instantiating class '"
                    + jobDetail.getJobClass().getName() + "'", e);
            throw se;
        }

    }

}

HelloWorldJob.java

@Service
@PerLookup
public class HelloWorldJob implements Job {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @PostConstruct
    public void setup() {
        log.info("I'm born!");
    }

    @PreDestroy
    public void shutdown() {
        // it's never called... :-(
        log.info("And I'm dead again");
    }

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("Hello, world!");
    }

}

Solution

  • Similar to @jwells131313 suggestion, I have implemented a JobListener that destroy()s instances of jobs where appropriate. To facilitate that, I pass along the ServiceHandle in the job's DataMap.

    The difference is only that I'm quite happy with the @PerLookup scope.

    Hk2JobFactory.java:

    @Service
    public class Hk2JobFactory implements JobFactory {
        private final Logger log = LoggerFactory.getLogger(getClass());
    
        @Inject
        ServiceLocator serviceLocator;
    
        @Override
        public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {
    
            JobDetail jobDetail = bundle.getJobDetail();
            Class<? extends Job> jobClass = jobDetail.getJobClass();
            try {
                log.debug("Producing instance of job {} (class {})", jobDetail.getKey(), jobClass.getName());
    
                ServiceHandle sh = serviceLocator.getServiceHandle(jobClass);
                if (sh != null) {
                    Class scopeAnnotation = sh.getActiveDescriptor().getScopeAnnotation();
                    if (log.isTraceEnabled()) log.trace("Service scope is {}", scopeAnnotation.getName());
                    if (scopeAnnotation == PerLookup.class) {
                        // @PerLookup scope means: needs to be destroyed after execution
                        jobDetail.getJobDataMap().put(SERVICE_HANDLE_KEY, sh);
                    }
    
                    return jobClass.cast(sh.getService());
                }
    
                log.debug("Unable to instantiate job via ServiceLocator, returning unmanaged instance");
                return jobClass.newInstance();
    
            } catch (Exception e) {
                SchedulerException se = new SchedulerException(
                        "Problem instantiating class '"
                        + jobDetail.getJobClass().getName() + "'", e);
                throw se;
            }
    
        }
    
    }
    

    Hk2CleanupJobListener.java:

    public class Hk2CleanupJobListener extends JobListenerSupport {
        public static final String SERVICE_HANDLE_KEY = "hk2_serviceHandle";
        private final Map<String, String> mdcCopy = MDC.getCopyOfContextMap();
    
        @Override
        public String getName() {
            return getClass().getSimpleName();
        }
    
        @Override
        public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
            JobDetail jobDetail = context.getJobDetail();
    
            ServiceHandle sh = (ServiceHandle) jobDetail.getJobDataMap().get(SERVICE_HANDLE_KEY);
            if (sh == null) {
                if (getLog().isTraceEnabled()) getLog().trace("No serviceHandle found");
                return;
            }
    
            Class scopeAnnotation = sh.getActiveDescriptor().getScopeAnnotation();
            if (scopeAnnotation == PerLookup.class) {
                if (getLog().isTraceEnabled()) getLog().trace("Destroying job {} after it was executed (Class {})", 
                        jobDetail.getKey(), 
                        jobDetail.getJobClass().getName()
                );
                sh.destroy();
            }
    
        }
    
    }
    

    Both are registered with the Scheduler.