- Use Spring-Session-Hazelcast to replicate Http Session between Application Nodes
- Use Hazelcast Hibernate as Hibernate 2nd Level cache
- Use only one single Hazelcast instance auto-generated by Spring Boot
- Support Grails mutable Session objects (Flash messages, etc.)
- Spring-Boot automatically configures a 'hazelcastInstance' bean if Hazelcast dependency is found.
- We can configure hazelcast-hibernate to use an existing HazelcastInstance with hibernate.cache.hazelcast.instance_name config key.
- Unfortunately, Grails instantiates the hibernateDatastore bean before Spring-Boot generates the hazelcastInstance bean.
- Due to this, hazelcast-hibernate generates its own Hazelcast Instance, so you have 2 Hazelcast instances running in your application
Ensure the Spring-Boot generated hazelcastInstance bean is generated before the Grails hibernateDatastore bean.
resources.groovy:
// Ensure Spring-Boot hazelcastInstance is generated before hibernateDatastore
// See also HazelcastHibernateDatastoreBeanPostProcessor
BeanDefinition hibernateDatastoreBeanDefinition = getBeanDefinition('hibernateDatastore')
if (hibernateDatastoreBeanDefinition) {
def dependsOnList = ['hazelcastInstance'] as Set
if (hibernateDatastoreBeanDefinition.dependsOn?.length > 0) {
dependsOnList.addAll(hibernateDatastoreBeanDefinition.dependsOn)
}
hibernateDatastoreBeanDefinition.dependsOn = dependsOnList as String[]
}
HazelcastHibernateDatastoreBeanPostProcessor.groovy
/**
* BeanPostProcessor to set Hibernate Hazelcast Instance name.
* This PostProcessor sets the {@link HazelcastHibernateDatastoreBeanPostProcessor#HIBERNATE_HAZELCAST_INSTANCE_NAME_KEY }
* to the name taken from hazelcastInstance bean as SystemProperty.
* This way, hazelcast-hibernate uses the same instance as the Spring-Boot created hazelcastInstance.
*/
@Slf4j
@CompileStatic
@Component
class HazelcastHibernateDatastoreBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware, PriorityOrdered {
private final static String HIBERNATE_HAZELCAST_INSTANCE_NAME_KEY = "hibernate.cache.hazelcast.instance_name"
private ApplicationContext applicationContext
private int order = LOWEST_PRECEDENCE - 2
@Override
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
log.trace("*** Postprocess {}", beanName)
if (bean instanceof HazelcastInstance){
String hazelcastInstanceName = applicationContext.getBean("hazelcastInstance", HazelcastInstance)?.name
log.info("Setting hibernate.cache.hazelcast.instance_name to $hazelcastInstanceName")
System.setProperty(HIBERNATE_HAZELCAST_INSTANCE_NAME_KEY, hazelcastInstanceName)
}
return bean
}
@Override
int getOrder() {
return order
}
@Override
void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext
}
}
Mutable objects in session are considered bad practice, but Grails4 uses them at least in:
- Flash Messages
- Form Tokens
To workaround this, we reconfigure the Hazelcast sessionRepository to re-save session keys on getAttribute(). See also: spring-projects/spring-session#177
@Slf4j
@CompileStatic
@Configuration
class HazelcastSymInstanceConfiguration {
/**
* Reconfigure SessionRepository to re-save all session keys which have been obtained using getAttribute().
* This way we ensure mutable Session objects are updated coherently in the Hazelcast cache.
* Mutable objects in session are considered bad practice, but Grails uses them at least in:
* - Flash Messages
* - Form Tokens
* {@see https://github.com/spring-projects/spring-session/issues/177}
*
* @return Customizer
*/
@Bean
SessionRepositoryCustomizer<HazelcastIndexedSessionRepository> customize() {
return new SessionRepositoryCustomizer<HazelcastIndexedSessionRepository>() {
@Override
void customize(HazelcastIndexedSessionRepository sessionRepository) {
log.info("Re-Configuring SessionRepositoryCustomizer to re-save session keys on getAttribute")
sessionRepository.setFlushMode(FlushMode.ON_SAVE)
sessionRepository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE)
}
}
}
}