Spring Redis Cache Service on Heroku


1. Setup Heroku RedisConnectionFactory Configuration
2. Setup RedisTemplate Configuration
3. Setup Redis Service
4. Setup Redis Controller

1. Setup Heroku RedisConnectionFactory Configuration

package com.example.redis;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;

import redis.clients.jedis.Protocol;

@Configuration
@Profile("default")
public class LocalRedisConfiguration {
	
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
    	
		try {
			URI redisUri = new URI(System.getenv("REDISCLOUD_URL"));
			JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
			redisConnectionFactory.setHostName(redisUri.getHost());
			redisConnectionFactory.setPort(redisUri.getPort());
			redisConnectionFactory.setTimeout(Protocol.DEFAULT_TIMEOUT);
			redisConnectionFactory.setPassword(redisUri.getUserInfo().split(":",2)[1]);
			
        return redisConnectionFactory;
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
    }
}

2. Setup RedisTemplate Configuration

package com.example.redis;

import javax.inject.Inject;

import org.springframework.context.annotation.*;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@Import({LocalRedisConfiguration.class})
public class RedisConfiguration {
	
	@Inject private RedisConnectionFactory redisConnectionFactory;
	
    @Bean
    public Topic topic() {
        return new ChannelTopic("pubsub:customer");
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(Topic topic, RedisConnectionFactory redisConnectionFactory, TaskExecutor taskExecutor) {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setTaskExecutor(taskExecutor);
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
        redisMessageListenerContainer.addMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message, byte[] pattern) {
                System.out.println("Received notification " + new String(message.getBody()) + ".");
                System.out.println(new String(pattern));
            }
        }, topic);
        return redisMessageListenerContainer;
    }

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setQueueCapacity(1);
        taskExecutor.setCorePoolSize(10);
        return taskExecutor;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() throws Exception {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

3. Setup Redis Service

package com.example.redis;

import com.example.service.CustomerService;
import com.example.model.Customer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigInteger;
import java.util.*;

@Service
public class RedisCustomerService implements CustomerService {

	@Autowired
	private RedisTemplate<String, Object> redisTemplate; 

    private String uniqueIdKey = "customerId";
 
    private BigInteger uniqueId() {
        long uniqueId = this.redisTemplate.opsForValue().increment(uniqueIdKey, 1);
        return BigInteger.valueOf(uniqueId);
    }
   
    private String lastNameKey(BigInteger id) {
        return "customer:ln:" + id;
    }

    private String firstNameKey(BigInteger id) {
        return "customer:fn:" + id;
    }

    public Collection<Customer> loadAllCustomers() {
        String keyPattern = "customer:fn:*";
        Set<String> keys = redisTemplate.keys(keyPattern);
        Set<Customer> customerSet = new HashSet<Customer>();
        for (String firstNameKey : keys) {
            long id = Long.parseLong(firstNameKey.split(":")[2]);
            customerSet.add(this.getCustomerById(BigInteger.valueOf(id)));
        }
        return customerSet;
    }

	@Override
	public Customer updateCustomer(BigInteger id, String fn, String ln) {
		 this.redisTemplate.opsForValue().set(lastNameKey(id), ln);
	     this.redisTemplate.opsForValue().set(firstNameKey(id), fn);
	     return getCustomerById(id);
	}

	@Override
	public List<Customer> updateCustomers(List<Customer> customersToUpdate) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Customer getCustomerById(BigInteger id) {
		String ln = (String) this.redisTemplate.opsForValue().get(lastNameKey(id));
	    String fn = (String) this.redisTemplate.opsForValue().get(firstNameKey(id));
	    return new Customer(id, fn, ln);
	}

	@Override
	public Collection<Customer> getAllCustomers() {
		 String keyPattern = "customer:fn:*";
	        Set<String> keys = redisTemplate.keys(keyPattern);
	        Set<Customer> customerSet = new HashSet<Customer>();
	        for (String firstNameKey : keys) {
	            long id = Long.parseLong(firstNameKey.split(":")[2]);
	            customerSet.add(this.getCustomerById(BigInteger.valueOf(id)));
	        }
	      return customerSet;
	}

	@Override
	public void deleteCustomer(BigInteger id) {
		redisTemplate.opsForValue().getOperations().delete(String.valueOf(id));
	}
	

	@Override
	public int getTotalRecords() {
		return redisTemplate.keys("customer:fn:*").size();
	}

	 private void setCustomerValues(BigInteger lid, String fn, String ln) {
	        this.redisTemplate.opsForValue().set(lastNameKey(lid), ln);
	        this.redisTemplate.opsForValue().set(firstNameKey(lid), fn);
	 }
	
	@Override
	public Customer createCustomer(String fn, String ln) {
		BigInteger lid = uniqueId();
        setCustomerValues(lid, fn, ln);
        return getCustomerById(lid);
	}
}

4. Setup Redis Controller

package com.example.controller;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Map;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.redis.RedisCustomerService;

@Controller
@RequestMapping("/redis")
public class RedisController {

	@Autowired
	RedisCustomerService redisCustomerService;
	
	@RequestMapping("")
	public String redisView(Map<String, Object> map){
		map.put("redisCustomerList", redisCustomerService.getAllCustomers());
		return "redis";
	}
	
	@RequestMapping(value="/insert")
	public String insertCustomersView(){
		return "insertredis";
	}
	
	@RequestMapping(value="/insert", method=RequestMethod.POST)
	public String insertCustomers(Map<String, Object> map, @RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName) {
		redisCustomerService.createCustomer(firstName, lastName);
		return "redis";
	}
	
	@RequestMapping(value="/delete/{id}", method=RequestMethod.POST)
	public String deleteCustomer(@PathVariable("id") String id, Map<String, Object> map){
		redisCustomerService.deleteCustomer(new BigInteger(id));
		map.put("redisCustomerList", redisCustomerService.getAllCustomers());
		return "redis";
	}
	
	@RequestMapping(value="/update/{id}", method=RequestMethod.GET)
	public String updateCustomerView(@PathVariable("id") String id, Map<String, Object> map){
		map.put("redisupdate", Arrays.asList(redisCustomerService.getCustomerById(new BigInteger(id))));
		return "updateredis";
	}
	
	@RequestMapping(value="/update/{id}", method=RequestMethod.POST)
	public String updateCustomer(@PathVariable("id") String id, Map<String, Object> map, @RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName){
		redisCustomerService.updateCustomer(new BigInteger(id), firstName, lastName);
		map.put("redisCustomerList", redisCustomerService.getAllCustomers());
		return "redis";
	}
}

Spring Quartz Service Implementation

Spring Service implementation of Quartz

package com.example.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.example.job.QuartzDeleteJob;
import com.example.job.QuartzInsertJob;

@Service
public class JobManagerService {

	public static final String BATCHINSERT = "QuartzInsertJob";
	public static final String BATCHDELETE = "QuartzDeleteJob";
	public static final String BATCHUPDATE = "QuartzUpdateJob";
	
	private Logger logger = LoggerFactory.getLogger(JobManagerService.class);
	
	private Scheduler scheduler = null;
	private Map<String, JobDetail> jobDetails = new HashMap<String, JobDetail>();
	private Map<String, Trigger> jobTriggers = new HashMap<String, Trigger>();
	private Map<String, JobKey> jobKeys = new HashMap<String, JobKey>();
	private Map<String, String> cronExpressions = new HashMap<String, String>();
	private List<String> cronOutput = new ArrayList<String>();
	
	private boolean successfullyInitialized = false;

	@PostConstruct
	public void initialize() {
		try {
			initializeScheduler();
			initializeJob(QuartzInsertJob.class, BATCHINSERT);
			initializeJob(QuartzDeleteJob.class, BATCHDELETE);
			successfullyInitialized = true;
		} catch (SchedulerException e) {
			logger.warn("exception during initialization");
			e.printStackTrace();
		}
	}
	
	private void initializeScheduler() throws SchedulerException {
		logger.info("initializing scheduler...");
		scheduler = StdSchedulerFactory.getDefaultScheduler();
		scheduler.start();
		logger.info("scheduler initialized and started");
	}

	public JobDetail buildJob(Class<?> jobClass, String jobName){
		JobDetail jobDetail =  JobBuilder.newJob((Class<Job>) jobClass).withIdentity(jobName).storeDurably(true).build();
		jobDetails.put(jobName, jobDetail);
		return jobDetail;
	}
	
	public void getJobKeyAndCheckExist(JobDetail jobDetail, String jobName) throws SchedulerException{
		JobKey jobKey = jobDetail.getKey();
		jobKeys.put(jobName, jobKey);
		
		if (scheduler.checkExists(jobKey)) {
			jobDetail = scheduler.getJobDetail(jobKey);
		
			List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobDetail.getKey());
			
			if (triggers.size() > 0) {
				if (triggers.size() > 1) {
					resetTriggers(jobName, triggers);
				} else {
					Trigger trigger = triggers.get(0);
					jobTriggers.put(jobName, trigger);
					
					cronExpressions.put(jobName, ((CronTrigger) trigger).getCronExpression());
				}
			}
		} else {
			scheduler.addJob(jobDetail, false);
		}
		
	}
	
	private void initializeJob(Class<?> jobClass, String jobName) throws SchedulerException {
		logger.info("initializing <{}>", jobName);
		JobDetail jobDetail = buildJob(jobClass, jobName);
		getJobKeyAndCheckExist(jobDetail, jobName);
		logger.info("<{}> initialized", jobName);
	}
	
	private void resetTriggers(String jobName, List<? extends Trigger> triggers) throws SchedulerException {			
		for (Trigger trigger : triggers) {
			scheduler.unscheduleJob(trigger.getKey());
		}
	}
	
	public String checkAndUnscheduleTrigger(String jobName) {
		Trigger trigger = jobTriggers.get(jobName);
		
		if (trigger != null) {
			TriggerKey triggerKey = trigger.getKey();
			
			logger.info("unscheduling <{}>", triggerKey);
			
			try {
				scheduler.unscheduleJob(trigger.getKey());
				jobTriggers.put(jobName, null);
				return triggerKey + " successfully unscheduled";
			} catch (SchedulerException e) {
				logger.error("exception unscheduling <{}>", triggerKey);
				e.printStackTrace();
			}
		} else {
			logger.info("trigger for <{}> not found", jobName);
		}
		return "exception unscheduling";
	}
	
	public List<String> checkAndScheduleCronExpression(String jobName, String triggerName, String cronExpression) {
		Trigger trigger = jobTriggers.get(jobName);
		
		if (trigger == null) {
			trigger = TriggerBuilder.newTrigger()
					.withIdentity(triggerName)
					.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
					.forJob(jobDetails.get(jobName)).build();
			
			try {
				cronOutput.add("scheduling "  + jobName + " with cron expression " + cronExpression);
				scheduler.scheduleJob(trigger);
				cronOutput.add("successfully scheduled: " + jobName);
				
				jobTriggers.put(jobName, trigger);
				cronExpressions.put(jobName, cronExpression);
			} catch (SchedulerException e) {
				cronOutput.add("exception scheduling: "+ jobName);
				e.printStackTrace();
			}
		} else {
			cronOutput.add("trigger already exists for: " + jobName);
		}
		
		return cronOutput;
	}
	
	public boolean isSuccessfullyInitialized() {
		return successfullyInitialized;
	}

	public String retrieveCronExpression(String jobName) {
		return cronExpressions.get(jobName);
	}
	
	public Map<String, String> getCronExpressions(){
		return cronExpressions;
	}
	
	public Map<String, Trigger> getJobTriggers(){
		return jobTriggers;
	}
	
	public Map<String, JobDetail> getJobDetails(){
		return jobDetails;
	}
	
	public Map<String, JobKey> getJobKeys(){
		return jobKeys;
	}
	
	public List<String> getCurrentlyExecutingJobs() throws SchedulerException{
		List<String> jobDescriptions = new ArrayList<String>();
		for (JobExecutionContext runningJobs : scheduler.getCurrentlyExecutingJobs()){
			jobDescriptions.add(runningJobs.getJobDetail().getDescription());
		}
		return jobDescriptions;
	}
}

Example of an Insert Quartz job to insert records into a database using jpa.

package com.example.job;

import java.util.List;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.example.jpa.JpaConfiguration;
import com.example.model.Customer;
import com.example.service.CustomerService;

public class QuartzDeleteJob implements Job  {

	@Override
	public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
		  
	        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
	        applicationContext.getEnvironment().setActiveProfiles("default");
	        applicationContext.scan(JpaConfiguration.class.getPackage().getName());
	        applicationContext.refresh();
	        CustomerService customerService = applicationContext.getBean(CustomerService.class);
	        
	        List<Customer> listOfCustomersToDelete = customerService.getPaginatedCustomer(0, 2);
	        customerService.deleteCustomers(listOfCustomersToDelete);
	}
}