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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user