Fix class loading problem with DevTools and Spring Session with Redis

Previously, when a session attribute that had been stored in Redis was
being deserialized, the app class loader would be used. This would
result in a ClassCastException if the attribute was an instance of a
class visible to the restart class loader.

This commit auto-configures Spring Session’s RedisTemplate to use a
custom deserializer that uses the restart class loader when
deserializing session attributes.

Closes gh-3805
pull/4327/merge
Andy Wilkinson 9 years ago
parent 17c4fa9d81
commit e4342075ab

@ -54,6 +54,16 @@
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<optional>true</optional>
</dependency>
<!-- Annotation processing -->
<dependency>
<groupId>org.springframework.boot</groupId>

@ -22,6 +22,7 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -40,12 +41,15 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.ExpiringSession;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for local development support.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
*/
@Configuration
@ -159,6 +163,19 @@ public class LocalDevToolsAutoConfiguration {
return watcher;
}
@Configuration
@ConditionalOnBean(name = "sessionRedisTemplate")
static class RedisRestartConfiguration {
@Bean
public RestartCompatibleRedisSerializerConfigurer restartCompatibleRedisSerializerConfigurer(
RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
return new RestartCompatibleRedisSerializerConfigurer(
sessionRedisTemplate);
}
}
}
}

@ -0,0 +1,110 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.devtools.autoconfigure;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.serializer.DefaultDeserializer;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.session.ExpiringSession;
import org.springframework.util.ObjectUtils;
/**
* Configures a {@link RedisTemplate} with a serializer for keys, values, hash keys, and
* hash values that is compatible with the split classloader used for restarts.
*
* @author Andy Wilkinson
* @author Rob Winch
*
* @see RedisTemplate#setHashKeySerializer(RedisSerializer)
* @see RedisTemplate#setHashValueSerializer(RedisSerializer)
* @see RedisTemplate#setKeySerializer(RedisSerializer)
* @see RedisTemplate#setValueSerializer(RedisSerializer)
*/
class RestartCompatibleRedisSerializerConfigurer implements BeanClassLoaderAware {
private final RedisTemplate<String, ExpiringSession> redisTemplate;
private volatile ClassLoader classLoader;
RestartCompatibleRedisSerializerConfigurer(
RedisTemplate<String, ExpiringSession> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@PostConstruct
void configureTemplateSerializers() {
RestartCompatibleRedisSerializer serializer = new RestartCompatibleRedisSerializer(
this.classLoader);
this.redisTemplate.setHashKeySerializer(serializer);
this.redisTemplate.setHashValueSerializer(serializer);
this.redisTemplate.setKeySerializer(serializer);
this.redisTemplate.setValueSerializer(serializer);
}
static class RestartCompatibleRedisSerializer implements RedisSerializer<Object> {
private final Converter<Object, byte[]> serializer = new SerializingConverter();
private final Converter<byte[], Object> deserializer;
RestartCompatibleRedisSerializer(ClassLoader classLoader) {
this.deserializer = new DeserializingConverter(
new DefaultDeserializer(classLoader));
}
@Override
public Object deserialize(byte[] bytes) {
if (ObjectUtils.isEmpty(bytes)) {
return null;
}
try {
return this.deserializer.convert(bytes);
}
catch (Exception ex) {
throw new SerializationException("Cannot deserialize", ex);
}
}
@Override
public byte[] serialize(Object object) {
if (object == null) {
return new byte[0];
}
try {
return this.serializer.convert(object);
}
catch (Exception ex) {
throw new SerializationException("Cannot serialize", ex);
}
}
}
}

@ -31,6 +31,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.devtools.autoconfigure.RestartCompatibleRedisSerializerConfigurer.RestartCompatibleRedisSerializer;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
@ -44,6 +45,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.ExpiringSession;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.SocketUtils;
@ -63,6 +67,7 @@ import static org.mockito.Mockito.verify;
* Tests for {@link LocalDevToolsAutoConfiguration}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class LocalDevToolsAutoConfigurationTests {
@ -235,6 +240,24 @@ public class LocalDevToolsAutoConfigurationTests {
assertThat(folders, hasKey(new File("src/test/java").getAbsoluteFile()));
}
@Test
public void sessionRedisTemplateIsConfiguredWithCustomDeserializers()
throws Exception {
SpringApplication application = new SpringApplication(
SessionRedisTemplateConfig.class, LocalDevToolsAutoConfiguration.class);
application.setWebEnvironment(false);
this.context = application.run();
RedisTemplate<?, ?> redisTemplate = this.context.getBean(RedisTemplate.class);
assertThat(redisTemplate.getHashKeySerializer(),
is(instanceOf(RestartCompatibleRedisSerializer.class)));
assertThat(redisTemplate.getHashValueSerializer(),
is(instanceOf(RestartCompatibleRedisSerializer.class)));
assertThat(redisTemplate.getKeySerializer(),
is(instanceOf(RestartCompatibleRedisSerializer.class)));
assertThat(redisTemplate.getValueSerializer(),
is(instanceOf(RestartCompatibleRedisSerializer.class)));
}
private ConfigurableApplicationContext initializeAndRun(Class<?> config,
String... args) {
return initializeAndRun(config, Collections.<String, Object>emptyMap(), args);
@ -282,4 +305,16 @@ public class LocalDevToolsAutoConfigurationTests {
}
@Configuration
public static class SessionRedisTemplateConfig {
@Bean
public RedisTemplate<String, ExpiringSession> sessionRedisTemplate() {
RedisTemplate<String, ExpiringSession> redisTemplate = new RedisTemplate<String, ExpiringSession>();
redisTemplate.setConnectionFactory(mock(RedisConnectionFactory.class));
return redisTemplate;
}
}
}

Loading…
Cancel
Save