I am trying to create a module for running report tasks based on configuration. The thought is to pass report configuration to a single task for defining each report specifics. Report configuration contains data information and class names that will be invoked to do the separate tasks. For this I have a Scheduling Service for dynamically configuring on startup my scheduled tasks. Task (runnable impl) is ReportExecutor.
@Service
public class ReportSchedulingService {
private static final Logger logger = LoggerFactory.getLogger(ReportSchedulingService.class);
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
@Autowired
private ReportList reportList;
private static Map<String, ScheduledFuture<?>> jobsMap = new HashMap<String, ScheduledFuture<?>>();
public void scheduleAllReports() {
for (Map.Entry<String, Report> reportEntry : reportList.getReports().entrySet()) {
String reportName = reportEntry.getKey();
Report report = reportEntry.getValue();
String jobId = UUID.randomUUID().toString();
logger.info(String.format("Scheduling report [%s] with job id: [%s] and cron expression: [%s]",
reportName, jobId, report.getCron()));
ReportExecutor execution = new ReportExecutor();
report.setId(jobId);
report.setName(reportName);
execution.setTaskDefinition(report);
ScheduledFuture<?> scheduledTask = taskScheduler.schedule(execution,
new CronTrigger(report.getCron(), TimeZone.getTimeZone(TimeZone.getDefault().getID())));
getJobMap().put(reportName "_" jobId, scheduledTask);
}
}
private synchronized Map<String, ScheduledFuture<?>> getJobMap() {
if (jobsMap == null) {
jobsMap = new HashMap<String, ScheduledFuture<?>>();
}
return jobsMap;
}
public void removeScheduledTask(String jobId) {
ScheduledFuture<?> scheduledTask = getJobMap().get(jobId);
if (scheduledTask != null) {
scheduledTask.cancel(true);
getJobMap().put(jobId, null);
}
}
}
Report Executor is:
@Component
public class ReportExecutor implements Runnable {
@Autowired
private ApplicationContext appContext;
private Report reportDefinition;
@Override
public void run() {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
System.out.println(reportDefinition);
IReportGenerator generator = appContext.getBean(
reportDefinition.getExecute(),
IReportGenerator.class);
JSONArray reportData = generator.generate(reportDefinition);
IReportNotifier notifier =
appContext.getBean(reportDefinition.getNotification().getExecute(), IReportNotifier.class);
notifier.send(reportDefinition, reportData);
}
public Report getTaskDefinition() {
return reportDefinition;
}
public void setTaskDefinition(Report reportDefinition) {
this.reportDefinition = reportDefinition;
}
}
But I have also tried with:
@Component
public class ReportExecutor implements Runnable, ApplicationContextAware {
private ApplicationContext appContext;
private Report reportDefinition;
@Override
public void run() {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
System.out.println(reportDefinition);
IReportGenerator generator = appContext.getBean(
reportDefinition.getExecute(),
IReportGenerator.class);
JSONArray reportData = generator.generate(reportDefinition);
IReportNotifier notifier =
appContext.getBean(reportDefinition.getNotification().getExecute(), IReportNotifier.class);
notifier.send(reportDefinition, reportData);
}
public Report getTaskDefinition() {
return reportDefinition;
}
public void setTaskDefinition(Report reportDefinition) {
this.reportDefinition = reportDefinition;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.appContext = applicationContext;
}
}
Seems that I cannot get the application context here and end up to a NPE at:
IReportGenerator generator = appContext.getBean(
The stackTrace is:
java.lang.NullPointerException: null
at ote.itarc.report.ReportExecutor.run(ReportExecutor.java:27) ~[classes/:?]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) [spring-context-5.2.10.RELEASE.jar:5.2.10.RELEASE]
at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:93) [spring-context-5.2.10.RELEASE.jar:5.2.10.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) [?:?]
at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) [?:?]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
at java.lang.Thread.run(Thread.java:836) [?:?]
ReportList object comes from yaml properties:
reportlist:
reports:
missinginfra:
desc: "This report provides ..."
execute: QueryReportGenerator
query: SELECT * FROM DUAL
cron: "0 0/2 * 1/1 * ?"
notification:
execute: EmailCsvReportNotifier
templateid: 0
With the following definition
@ConfigurationProperties(prefix = "reportlist")
@Configuration
@EnableConfigurationProperties
public class ReportList {
private Map<String, Report> reports;
public Map<String, Report> getReports() {
return reports;
}
public void setReports(Map<String, Report> reports) {
this.reports = reports;
}
@Override
public String toString() {
return "ReportList [reports=" reports "]";
}
}
CodePudding user response:
Make the ReportExecutor
a Spring managed bean and make it prototype scoped. Next in your ReportSchedulingService
use the ApplicationContext
to get an instance (and let Spring do the wiring) and schedule it.
@Service
public class ReportSchedulingService {
private static final Logger logger = LoggerFactory.getLogger(ReportSchedulingService.class);
private final Map<String, ScheduledFuture<?>> jobsMap = new ConcurrentHashMap<String, ScheduledFuture<?>>();
private final TaskScheduler taskScheduler;
private final ReportList reportList;
private final ApplicationContext ctx;
public ReportSchedulingService(ReportList reportList, TaskScheduler taskScheduler, ApplicationContext ctx) {
this.reportList=reportList;
this.taskScheduler=taskScheduler;
this.ctx=ctx;
}
public void scheduleAllReports() {
for (Map.Entry<String, Report> reportEntry : reportList.getReports().entrySet()) {
String reportName = reportEntry.getKey();
Report report = reportEntry.getValue();
String jobId = UUID.randomUUID().toString();
logger.info(String.format("Scheduling report [%s] with job id: [%s] and cron expression: [%s]",
reportName, jobId, report.getCron()));
ReportExecutor execution = ctx.getBean(ReportExecutor.class);
report.setId(jobId);
report.setName(reportName);
execution.setTaskDefinition(report);
ScheduledFuture<?> scheduledTask = taskScheduler.schedule(execution,
new CronTrigger(report.getCron(), TimeZone.getTimeZone(TimeZone.getDefault().getID())));
this.jobsMap.put(reportName "_" jobId, scheduledTask);
}
}
public void removeScheduledTask(String jobId) {
ScheduledFuture<?> scheduledTask = this.jobsMap.get(jobId);
if (scheduledTask != null) {
scheduledTask.cancel(true);
getJobMap().put(jobId, null);
}
}
}
NOTE: I took the liberty to improve your code a little as well, using constructor injection and using a ConcurrentHashMap
instead of syncronized
.
@Component
@Scope(scopeName=SCOPE_PROTOTYPE, proxyMode=TARGET_CLASS)
public class ReportExecutor implements Runnable {
private final ApplicationContext appContext;
private Report reportDefinition;
public ReportExecutor(ApplicationContext ctx) {
this.appContext=ctx;
}
@Override
public void run() {
IReportGenerator generator = appContext.getBean(
reportDefinition.getExecute(),
IReportGenerator.class);
JSONArray reportData = generator.generate(reportDefinition);
IReportNotifier notifier =
appContext.getBean(reportDefinition.getNotification().getExecute(), IReportNotifier.class);
notifier.send(reportDefinition, reportData);
}
public void setTaskDefinition(Report reportDefinition) {
this.reportDefinition = reportDefinition;
}
}
The scoped proxy will create a new instance for each call to getBean
. So you get a fresh, clean instance for each execution you want to schedule.