關於Quartz的Job 不能被注入以及SpringAop對Job失效

JasonTam發表於2019-02-25

關於Quartz的Job 不能被注入以及SpringAop對Job失效

Problem(問題)

​ 最近在工作遇到需要對Quartz的Job進行異常後將異常記錄到資料庫的操作,第一反應就想到了使用Spring的AOP,利用AfterThrowing來完成這個操作。理想是美好的,但現實卻是骨感的。研究了好久都不生效。研究的過程發現居然還不能依賴注入,注入到的testService是空的。

​ 切面類(Aspect)

@Aspect
public class ErrorLogAop {

    @Pointcut("execution(public * com.aspect.quartzdemo.job.*.*(..))")
    //@Pointcut("execution(public * com.aspect.quartzdemo.service.*.*(..))")
    public void joinPointExpression(){}


    @Before("joinPointExpression()")
    public void before(){
        System.out.println("start before");
    }

}
複製程式碼

​ Job(任務類)

public class TimeJob implements Job {

    @Autowired
    private TestService testService;
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        testService.test();
        System.out.println(new Date());
    }
}
複製程式碼

Environment(環境)

​ 該篇文章圍繞的是Springboot與Quartz一起使用時發生的問題。

​ JDK: 1.8

​ SpringBoot:2.0.4.RELEASE

​ Quartz:spring-boot-starter-quartz(由於Springboot2.0後官方主動與Quartz整合了,並推出了啟動器,所以不需要以前那麼繁瑣的配置了,什麼工廠什麼例項一堆的。現在Scheduler,Springboot直接幫我們生成好了,我們只需要依賴注入就可以了。)

Solution(解法)

​ 最後根據幾個大神的幫助,解決了這個問題。主要是因為Job的例項是由Quartz進行管理的,因而Spring管理的例項並不能在Quartz例項Job的過程進行任何操作。下面介紹兩種解決的方案。

1. 使用JobListener監聽任務內的操作

Listeners are objects that you create to perform actions based on events occurring within the scheduler. As you can probably guess, TriggerListeners receive events related to triggers, and JobListeners receive events related to jobs.

Trigger-related events include: trigger firings, trigger mis-firings (discussed in the “Triggers” section of this document), and trigger completions (the jobs fired off by the trigger is finished).

官方文件的意思大概是:監聽器是在排程器中基於事件機制執行操作的物件,我們大概可以猜到,觸發監聽器是接收跟觸發器相關的事件,任務監聽器是跟接收跟任務有關的事件。

org.quartz.TriggerListener implement:

​ 跟觸發器有關的事件包括:觸發器被觸發,觸發器觸發失敗以及觸發器觸發完成(觸發器完成後作業任務開始執行)

public interface TriggerListener {  

    public String getName();  

    public void triggerFired(Trigger trigger, JobExecutionContext context);  

    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context);  

    public void triggerMisfired(Trigger trigger);  

    public void triggerComplete(Trigger trigger, JobExecutionContext context,  
    int triggerInstructionCode);
}  
複製程式碼

org.quartz.JobListener implement:

跟作業任務相關的事件:job即將被執行的通知和job執行完成的通知事件(其中包括出現異常的通知,而且還能捕獲到異常的資訊)這個就很符合我的想法了^_^。

public interface JobListener {  

    public String getName();  

    public void jobToBeExecuted(JobExecutionContext context);  

    public void jobExecutionVetoed(JobExecutionContext context);  

    public void jobWasExecuted(JobExecutionContext context,  
    JobExecutionException jobException);  

}  
複製程式碼

那麼怎麼使用自定義的監聽器呢?來看看官方文件是怎麼說的

To create a listener, simply create an object that implements the org.quartz.TriggerListener and/or org.quartz.JobListener interface. Listeners are then registered with the scheduler during run time, and must be given a name (or rather, they must advertise their own name via their getName() method).

For your convenience, tather than implementing those interfaces, your class could also extend the class JobListenerSupport or TriggerListenerSupport and simply override the events you’re interested in.

意思大概是:建立監聽器只需要實現JobListener或者TriggerListener介面就能實現了,監聽器會向排程器進行註冊,而且還必須給監聽器一個名字。(它會自動在getName()這個方法中獲取)。

為了方便我們,我們可以選擇只繼承JobListenerSupport或TriggerListenerSupport來重寫我們所需要的方法。

但是官方給出來的例子比較模糊,我提供一個算是比較簡單但又比較全的栗子吧。

Implement JobListener interface:(Create a Class implement JobListener interface)
public class MyJobListener implements JobListener {

    @Override
    public String getName() {
        return "myJobListener";
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println("start Job");
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println("Job was executed");
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println("Job is Done");
        System.out.println("Job is Error");
    }
}
複製程式碼
JobConfiguration:(Listeners are registered with the scheduler during run time)
@Configuration
public class JobConfig {

    @Autowired private Scheduler scheduler;

    @PostConstruct
    public void addListener() throws SchedulerException {
        MyJobListener myJobListener = new MyJobListener();
        // Add the All Job to the Scheduler(全部)
        scheduler.getListenerManager().addJobListener(myJobListener, allJobs());
        // Adding a JobListener that is interested in all jobs of a particular group(一個)
        scheduler.getListenerManager().addJobListener(myJobListener, jobGroupEquals("myJobGroup"));
        // Adding a JobListener that is interested in all jobs of two particular groups(兩個)
scheduler.getListenerManager().addJobListener(myJobListener,or(jobGroupEquals("myJobGroup"),                   jobGroupEquals("yourGroup")));
    }

    @Bean
    public JobDetail myJobDetail() {
        return JobBuilder.newJob(MyJob.class).withIdentity("MyJob")
                .storeDurably().build();
    }

    @Bean
    public Trigger myJobTrigger() {
  CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/2 * * * * ? *");
return TriggerBuilder.newTrigger().forJob(myJobDetail()).withIdentity("MyTrigger").withSchedule(cronScheduleBuilder).build();
    }
}    
複製程式碼

執行,看效果,收工。TriggerListener也是一樣這麼使用的。

Listeners are not used by most users of Quartz, but are handy when application requirements create the need for the notification of events, without the Job itself having to explicitly notify the application.

官方也說其實很少人使用(哈哈哈)。我就用到了,確實也挺便利的。

2. 把Job的例項交給Spring管理

​ 當我看到這個解決方案的時候,我深深的感覺到自己對於Spring知識的薄弱了。來看看吧。

   - Quartz提供了JobFactory介面,讓我們可以自定義實現建立Job的邏輯。
   - 通過實現JobFactory介面,在例項化Job以後,在通過ApplicationContext將Job所需要的屬性注入即可。
   - 在Spring與Quartz整合時,用到了org.springframework.scheduling.quartz.SchedulerFactoryBean這個類。
複製程式碼
// Get Scheduler instance from SchedulerFactory.
        try {
            this.scheduler = createScheduler(schedulerFactory, this.schedulerName);
            populateSchedulerContext();

            if (!this.jobFactorySet && !(this.scheduler instanceof RemoteScheduler)) {
                // Use AdaptableJobFactory as default for a local Scheduler, unless when
                // explicitly given a null value through the "jobFactory" bean property.
              |--  this.jobFactory = new AdaptableJobFactory(); --|
                  //這裡是重點。
            }
            if (this.jobFactory != null) {
                if (this.jobFactory instanceof SchedulerContextAware) {
                    ((SchedulerContextAware) this.jobFactory).setSchedulerContext(this.scheduler.getContext());
                }
                this.scheduler.setJobFactory(this.jobFactory);
            }
        }
複製程式碼
  • 由於將Scheduler交給Spring生成, SchedulerFactoryBean有個jobFactory屬性 而且jobFactory是實現SchedulerContextAware的類還要繼承AdaptableJobFactory。
  • 在Spirng-context-support jar包下org.springframework.scheduling.quartz包中有個SpringBeanJobFactory的類繼承了AdaptableJobFactory實現AdaptableJobFactory,spring會預設使用這個給jobFactory,我們可以繼承SpringBeanJobFactory重寫他的createJobInstance方法
public class JobBeanFactory extends SpringBeanJobFactory {

    @Nullable
    private String[] ignoredUnknownProperties;

    @Nullable
    private SchedulerContext schedulerContext;


    private final BeanFactory beanFactory;


    JobBeanFactory(BeanFactory beanFactory){
        this.beanFactory = beanFactory;
    }


    public void setIgnoredUnknownProperties(String... ignoredUnknownProperties) {
        this.ignoredUnknownProperties = ignoredUnknownProperties;
    }

    @Override
    public void setSchedulerContext(SchedulerContext schedulerContext) {
        this.schedulerContext = schedulerContext;
    }


    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        Class<?> jobClass = bundle.getJobDetail().getJobClass();
        Object job = beanFactory.getBean(jobClass);
        if (isEligibleForPropertyPopulation(job)) {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);
            MutablePropertyValues pvs = new MutablePropertyValues();
            if (this.schedulerContext != null) {
                pvs.addPropertyValues(this.schedulerContext);
            }
            pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());
            pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());
            if (this.ignoredUnknownProperties != null) {
                for (String propName : this.ignoredUnknownProperties) {
                    if (pvs.contains(propName) && !bw.isWritableProperty(propName)) {
                        pvs.removePropertyValue(propName);
                    }
                }
                bw.setPropertyValues(pvs);
            }
            else {
                bw.setPropertyValues(pvs, true);
            }
        }
        return job;
    }
}
複製程式碼

當Spring在載入配置檔案時,如果配置檔案中有Bean實現了ApplicationContextAware介面時,Spring會自動呼叫setApplicationContext方法,我們可以通過這個獲取Spring上下文然後在建立Job時讓Job自動注入到Spring容器中

在JobConfig中:

@Configuration
public class JobConfig implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Bean
    public JobDetail myJobDetail() {
        return JobBuilder.newJob(MyJob.class).withIdentity("MyJob")
                .storeDurably().build();
    }

    @Bean
    public Trigger myJobTrigger() {
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/2 * * * * ? *");
        return TriggerBuilder.newTrigger().forJob(myJobDetail()).withIdentity("MyTrigger").withSchedule(cronScheduleBuilder).build();
    }
    
     @Bean
    public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() {
        return schedulerFactoryBean -> schedulerFactoryBean.setJobFactory(new JobBeanFactory(applicationContext));
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
複製程式碼

之後就可以使用Spring的AOP了(記得把切面類也要交給Spring管理)。

@Aspect
@Component
public class ErrorLogAop {

    @Pointcut("execution(public * com.aspect.quartzdemo.job.*.*(..))")
    //@Pointcut("execution(public * com.aspect.quartzdemo.service.*.*(..))")
    public void joinPointExpression(){}


    @Before("joinPointExpression()")
    public void before(){
        System.out.println("start before");
    }

}
複製程式碼

Job也需要加上註解@Component

@Component
public class MyJob implements Job {


    @Override
    public void execute(JobExecutionContext context) {
        System.out.println("hello");
        System.out.println(this);

    }
}
複製程式碼

執行,看效果,收工(哈哈)。

總結

​ 首先感謝提供解決方案的各位大神,這個問題讓我覺得自己還有很多的不足,很多知識都是一知半解會用就行的感覺,繼續努力吧。

參考資料:

blog.csdn.net/a67474506/a…

www.cnblogs.com/daxin/p/360…

xuzongbao.gitbooks.io/quartz/cont…

相關文章