Full Spring integration with Quartz to implement real time notification e-mails

前端 未结 1 1357
无人及你
无人及你 2021-01-03 18:54

I\'m currently developing an application with spring boot that let users to create an appointment. So basically the appointment has a startDateTime and an endDateTime fields

相关标签:
1条回答
  • 2021-01-03 19:31

    You can use a scheduling library such as quartz, providing easy integration with Spring framework.

    After an appointment is saved in your database, a "send-email" job will be scheduled for the desirable time (one hour before start date for instance).

    A "send-email" job must implement org.quartz.Job and more specifically execute method where you can use your Autowired SendEmailService implementation.

    Below you can find a (almost) complete example of how such a requirement could be implemented in code.

    Update - Code to schedule the job

    First we define a SchedulingService interface.

    public interface SchedulingService {
    
        startScheduler() throws SchedulerException;
    
        void standbyScheduler() throws SchedulerException;
    
        void shutdownScheduler() throws SchedulerException;
    
        void scheduleJob(JobDetail jobDetail, Trigger trigger) throws SchedulerException;
    }
    

    And relevant implementation.

    @Service
    public class SchedulingServiceImpl implements SchedulingService {
    
        @Autowired
        private Scheduler scheduler;
    
        @Override
        public void startScheduler() throws SchedulerException {
            if (!scheduler.isStarted()) {
                scheduler.start();
            }
        }
    
        @Override
        public void standbyScheduler() throws SchedulerException {
            if (!scheduler.isInStandbyMode()) {
                scheduler.standby();
            }
        }
    
        @Override
        public void shutdownScheduler() throws SchedulerException {
            if (!scheduler.isShutdown()) {
                scheduler.shutdown();
            }
        }
    
        @Override
        public void scheduleJob(JobDetail jobDetail, Trigger trigger) throws SchedulerException {
            scheduler.scheduleJob(jobDetail, trigger);
        }
    }
    

    Then in AppointmentServiceImpl we have a method createAppointment() which calls scheduleSendEmailJob().

    @Service
    public class AppointmentServiceImpl implements AppointmentService {
    
        @Autowired
        private SchedulingService schedulingService;
    
        public void createAppointment(Appointment appointment) throws SchedulerException {
    
            // Save appointment to database
            // ...
    
            // Schedule send email job if appointment has been successfully saved
            scheduleSendEmailJob(appointment);
    
            return;
        }
    
        private void scheduleSendEmailJob(Appointment appointment) throws SchedulerException {
    
            JobDetail jobDetail = JobBuilder.newJob().ofType(SendEmailJob.class)
                .storeDurably()
                .withIdentity(UuidUtils.generateId(), "APPOINTMENT_NOTIFICATIONS")
                .withDescription("Send email notification for appointment")
                .build();
    
    
            jobDetail.getJobDataMap().put("appointmentId", appointment.getId());
    
            Date scheduleDate = appointment.computeDesiredScheduleDate();
            String cronExpression = convertDateToCronExpression(scheduleDate);
    
            CronTrigger trigger = TriggerBuilder.newTrigger().forJob(jobDetail)
                .withIdentity(UuidUtils.generateId(), "APPOINTMENT_NOTIFICATIONS")
                .withDescription("Trigger description")
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                .build();
    
            schedulingService.scheduleJob(jobDetail, trigger);
        }
    
        private String convertDateToCronExpression(Date date) {
    
            Calendar calendar = new GregorianCalendar();
    
            if (date == null) return null;
    
            calendar.setTime(date);
    
            int year = calendar.get(java.util.Calendar.YEAR);
            int month = calendar.get(java.util.Calendar.MONTH) + 1;
            int day = calendar.get(java.util.Calendar.DAY_OF_MONTH);
            int hour = calendar.get(java.util.Calendar.HOUR_OF_DAY);
            int minute = calendar.get(java.util.Calendar.MINUTE);
    
            return String.format("0 %d %d %d %d ? %d", minute, hour, day, month, year);
        }
    }
    

    Class SendEmailJob is an implementation of Job interface and responsible for sending emails using relevant services.

    Update - Code to pass parameter from scheduling method to actual job execution

    For passing parameters, jobDataMap is being used. For instance:

    public class SendEmailJob implements Job {
    
        @Autowired
        private AppointmentService appointmentService;
    
        @Autowired
        private SendEmailService sendEmailService;
    
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    
            JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
    
            // Retrieving passed parameters
            Long appointmentId = (Long) jobDataMap.get("appointmentId");
    
            Appointment appointment = appointmentService.findById(appointmentId);
    
            // Send email
            sendEmailService.sendEmail(appointment);
        }
    }
    

    Note: Appointment object could also been passed from scheduling method to actual job execution, you can just pass:

    jobDetail.getJobDataMap().put("appointment", appointment);
    

    And get:

    // Retrieving passed parameters
    Appointment appointment = (Appointment) jobDataMap.get("appointment");
    

    Update - Configuration code

    Bean scheduler is defined in a @Configuration class responsible for Quartz initialization.

    SchedulingConfiguration class is defined as:

    @Configuration
    public class SchedulingConfiguration {
    
        @Autowired
        private ApplicationContext applicationContext;
    
        @Bean
        public Scheduler scheduler() throws SchedulerException, IOException {
    
            StdSchedulerFactory factory = new StdSchedulerFactory();
            factory.initialize(new ClassPathResource("properties/quartz.properties").getInputStream());
    
            Scheduler scheduler = factory.getScheduler();
            scheduler.setJobFactory(springBeanJobFactory());
    
            return scheduler;
        }
    
        @Bean
        public SpringBeanJobFactory springBeanJobFactory() {
            AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
            jobFactory.setApplicationContext(applicationContext);
            return jobFactory;
        }
    
    }
    

    Our quartz.properties file lives in resources/properties folder. Note that job persistence database is an Oracle instance.

    # Configure Main Scheduler Properties
    org.quartz.scheduler.instanceName = AppScheduler
    org.quartz.scheduler.instanceId = AUTO
    
    # Configure ThreadPool
    org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
    org.quartz.threadPool.threadCount = 10
    org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
    
    # Configure JobStore
    org.quartz.jobStore.misfireThreshold = 60000
    org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
    org.quartz.jobStore.driverDelegateClass = 
    org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
    org.quartz.jobStore.tablePrefix = APP.QRTZ_
    org.quartz.jobStore.useProperties = false
    org.quartz.jobStore.dataSource = appDs
    org.quartz.jobStore.isClustered = true
    org.quartz.jobStore.clusterCheckinInterval = 20000
    
    # Configure Datasources
    org.quartz.dataSource.appDs.driver = oracle.jdbc.driver.OracleDriver
    org.quartz.dataSource.appDs.URL = jdbc:oracle:thin:@dbsrv:1521:appdb
    org.quartz.dataSource.appDs.user = db_user
    org.quartz.dataSource.appDs.password = db_pwd
    org.quartz.dataSource.appDs.maxConnections = 5
    org.quartz.dataSource.appDs.validationQuery = select 0 from dual
    

    The final step is to call scheduler methods in application context initialization as following (please note added methods in SchedulingService):

    public class SchedulingContextListener implements ServletContextListener {
    
        private static final Logger logger = LogManager.getLogger(SchedulingContextListener.class);
    
        private SchedulingService schedulingService(ServletContextEvent sce) {
            WebApplicationContext springContext = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
            return springContext.getBean(SchedulingService.class);
        }
    
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            try {
                this.schedulingService(sce).startScheduler();
            } catch (SchedulerException e) {
                logger.error("Error while Scheduler is being started", e);
            }
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent sce) {
            try {
                this.schedulingService(sce).shutdownScheduler();
            } catch (SchedulerException e) {
                logger.error("Error while Scheduler is being shutdown", e);
            }
        }
    }
    

    Note: SchedulingContextListener should be registered in servletContext in application initialization, depending on how Spring configuration is defined, either using Spring Boot or traditional Spring MVC Configuration.

    Hope that helps.

    0 讨论(0)
提交回复
热议问题