Embedded Redisson Node is a service where you can execute asynchronous tasks using Redis with Redisson. This library is not bound to Spring whatsoever, so if you want to run your tasks in the Spring context you need to bind it manually. However, using Spring Boot with Devtools you may encounter some class loading problems between Redisson Node and Spring context, what I’ve today. Here is a quick howto what’s this about and how to overcome this problem.
We start from running the Redisson Node in embedded mode from Spring bean @PostConstruct
:
@Configuration
public class RedissonConfiguration {
public static final Logger logger = LoggerFactory.getLogger(RedissonConfiguration.class);
@Autowired protected RedissonNode redissonNode;
@Bean
protected Codec redissonCodec() {
return new SerializationCodec(getClass().getClassLoader());
}
@Bean
public Config redissonConfig(Codec redissonCodec) {
return new Config().setCodec(redissonCodec); // TODO provide your Redisson Config as a bean here
}
@Bean
public RedissonClient redissonClient(Config redissonConfig) {
return Redisson.create(redissonConfig);
}
@Bean
public RedissonNode redissonNode(Config config, RedissonClient redissonClient) {
RedissonNodeConfig nodeConfig = new RedissonNodeConfig(config);
return RedissonNode.create(nodeConfig, redissonClient);
}
@PostConstruct
protected void init() {
logger.debug("Starting redisson node");
redissonNode.start();
}
@PreDestroy
protected void done() {
logger.debug("Shutting down redisson node");
redissonNode.shutdown();
}
}
And here we have the service that helps run asynchronous tasks using Redisson and the Redisson Node started before - in my implementation
I want to use RScheduledExecutorService
:
@Component
public class Scheduler {
protected static Scheduler instance;
@Autowired protected RedissonClient redisson;
protected RScheduledExecutorService executor;
public static Scheduler getInstance() {
return instance;
}
@PostConstruct
protected void init() {
executor = redisson.getExecutorService("myExecutor");
instance = this;
}
public void scheduleTask() {
executor.schedule(new SchedulerTask(), 5, TimeUnit.SECONDS);
}
public void execute(SchedulerTask task) {
System.out.println(task);
}
}
As you can see I expose Scheduler
bean in static instance
field to have access to both this instance and the Spring Context from my SchedulerTask
. This needs to be done in some way because our SchedulerTask
recovered from Redisson at the trigger time doesn’t have any access to Spring. Here is the task implementation using this static
instance field:
public class SchedulerTask implements Runnable, Serializable {
private static final long serialVersionUID = 4175488709486809257L;
public ScheduledEventTask() {
}
@Override
public void run() {
Scheduler.getInstance().execute(this);
}
}
Everything looks working and should work, but it doesn’t… When I use Spring Devtools (org.springframework.boot:spring-boot-devtools
dependency) in the project I have NullPointerException
when I want to use Scheduler.getInstance()
from SchedulerTask
. But why?
It turns out it’s the class loader problem. Devtools to support hot restarts uses its own RestartClassLoader
. So, when it creates Scheduler
class, after which I create Scheduler
class instance, the class loader is RestartClassLoader
. While for RedissonNode
the class loader is AppClassLoader
what’s one level up before RestartClassLoader
(it’s RestartClassLoader.getParent()
). This happens because RedissonNode
class comes from a jar library not from the project classes, and is not supported by Devtools hot redeploy feature.
This is a really hard to debug problem and the final culprit of this behavior is this code inside Redisson:
public class TasksRunnerService implements RemoteExecutorService {
private final ClassLoaderDelegator classLoader = new ClassLoaderDelegator();
private final Codec codec;
// [...]
public TasksRunnerService(CommandExecutor commandExecutor, RedissonClient redisson, Codec codec, String name, ConcurrentMap<String, ResponseEntry> responses) {
// [...]
try {
this.codec = codec.getClass().getConstructor(ClassLoader.class).newInstance(classLoader);
} catch (Exception e) {
throw new IllegalStateException("Unable to initialize codec with ClassLoader parameter", e);
}
}
}
The codec
instance created here will be then used to restore classes serialized in Redis. The problem is it uses current classloader (AppClassLoader
), not the one I wanted to use creating codec in RedissonConfiguration.redissonCodec()
. Even if you pass the right codec instance to this constructor, it will anyway create new codec with the current class loader :/
OK, so how to overcome this problem in Spring Boot app? They are aware of classloading problems with Spring Devtools. It’s possible to redeploy jar library using RestartClassLoader
each time Devtools restarts the project. To do this you need to add following file to META-INF/spring-devtools.properties
:
restart.include.redisson=redisson-.*.jar
Using this configuration RestartClassLoader
will become RedissonNode.class.getClassLoader()
and all tasks created by TasksRunnerService
will now have an access to static fields of your project classes.