From e4342075ab912ce3a51ccf19edc42e341368dccd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Dec 2015 11:58:40 +0000 Subject: [PATCH] Fix class loading problem with DevTools and Spring Session with Redis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- spring-boot-devtools/pom.xml | 10 ++ .../LocalDevToolsAutoConfiguration.java | 17 +++ ...rtCompatibleRedisSerializerConfigurer.java | 110 ++++++++++++++++++ .../LocalDevToolsAutoConfigurationTests.java | 35 ++++++ 4 files changed, 172 insertions(+) create mode 100644 spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RestartCompatibleRedisSerializerConfigurer.java diff --git a/spring-boot-devtools/pom.xml b/spring-boot-devtools/pom.xml index 6fe474293e..072af3653c 100644 --- a/spring-boot-devtools/pom.xml +++ b/spring-boot-devtools/pom.xml @@ -54,6 +54,16 @@ spring-security-web true + + org.springframework.data + spring-data-redis + true + + + org.springframework.session + spring-session + true + org.springframework.boot diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java index 2c916e6bbd..e4e156742f 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java @@ -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 sessionRedisTemplate) { + return new RestartCompatibleRedisSerializerConfigurer( + sessionRedisTemplate); + } + + } + } } diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RestartCompatibleRedisSerializerConfigurer.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RestartCompatibleRedisSerializerConfigurer.java new file mode 100644 index 0000000000..7640a0c278 --- /dev/null +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RestartCompatibleRedisSerializerConfigurer.java @@ -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 redisTemplate; + + private volatile ClassLoader classLoader; + + RestartCompatibleRedisSerializerConfigurer( + RedisTemplate 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 { + + private final Converter serializer = new SerializingConverter(); + + private final Converter 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); + } + } + + } + +} diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java index 8e4b0786d5..f3b5a1ee8b 100755 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfigurationTests.java @@ -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.emptyMap(), args); @@ -282,4 +305,16 @@ public class LocalDevToolsAutoConfigurationTests { } + @Configuration + public static class SessionRedisTemplateConfig { + + @Bean + public RedisTemplate sessionRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(mock(RedisConnectionFactory.class)); + return redisTemplate; + } + + } + }