在 SpringBoot 开发项目的过程中,使用到了 RedisTemplate 操作 Hash,读值时遇到关于类型转换的问题,于是编写了一个小的测试 demo,现在记录下来,以后有时间再深入研究。
-
项目结构如下:
进入项目根目录,使用 tree /f 命令输出目录结构如下:
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─example
│ │ │ └─demo
│ │ │ │ SpringBootRedisApplication.java
│ │ │ │
│ │ │ ├─config
│ │ │ │ RedisConfiguration.java
│ │ │ │
│ │ │ ├─domain
│ │ │ │ Permission.java
│ │ │ │ Role.java
│ │ │ │ User.java
│ │ │ │
│ │ │ └─service
│ │ │ │ RedisService.java
│ │ │ │ UserService.java
│ │ │ │
│ │ │ └─util
│ │ │ RedisKey.java
│ │ │
│ │ └─resources
│ │ │ application-dev.yml
│ │ │ application-prod.yml
│ │ │ application.yml
│ │ │
│ │ ├─static
│ │ └─templates
│ └─test
│ └─java
│ └─com
│ └─example
│ └─demo
│ │ SpringBootRedisApplicationTests.java
│ │
│ └─service
│ RedisServiceTest.java -
pom 文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>spring-boot-redis</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-boot-redis</name> <description>Demo project for Spring Boot Redis</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
RedisConfiguration 配置类如下:
package com.example.demo.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author smileorsilence */ @Configuration @EnableConfigurationProperties(RedisProperties.class) public class RedisConfiguration { @Autowired private RedisProperties redisProperties; @Bean public JedisConnectionFactory redisConnectionFactory() { JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory(); redisConnectionFactory.setHostName(redisProperties.getHost()); redisConnectionFactory.setPort(redisProperties.getPort()); redisConnectionFactory.setDatabase(redisProperties.getDatabase()); if (this.redisProperties.getPassword() != null) redisConnectionFactory.setPassword(redisProperties.getPassword()); if (this.redisProperties.getTimeout() > 0) redisConnectionFactory.setTimeout(redisProperties.getTimeout()); return redisConnectionFactory; } @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JsonRedisSerializer()); //for hash redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new JsonRedisSerializer()); return redisTemplate; } public static class JsonRedisSerializer implements RedisSerializer<Object> { private ObjectMapper objectMapper = new ObjectMapper(); public JsonRedisSerializer() { } @Override public byte[] serialize(Object o) throws SerializationException { if (o == null) { return new byte[0]; } else { try { return this.objectMapper.writeValueAsBytes(o); } catch (Exception e) { throw new SerializationException("Could not write JSON: " + e.getMessage()); } } } @Override public Object deserialize(byte[] bytes) throws SerializationException { if (bytes == null) { return null; } else { try { return this.objectMapper.readValue(bytes, Object.class); } catch (Exception e) { throw new SerializationException("Could not read JSON: " + e.getMessage()); } } } } }
-
RedisService 工具类如下:
package com.example.demo.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @author smileorsilence */ @Slf4j @Service public class RedisService { /***** ***** ***** ***** ***** ***** Operation for Key ***** ***** ***** ***** ***** *****/ @Autowired private RedisTemplate<String, Object> redisTemplate; public boolean hasKey(String key) { return redisTemplate.hasKey(key); } public void deleteKey(String key) { log.debug("Delete key in redis, key:{}", key); redisTemplate.delete(key); } public void expireKey(String key, Long times, TimeUnit unit) { log.debug("Expire key in redis, key:{}", key); redisTemplate.expire(key, times, unit); } /***** ***** ***** ***** ***** ***** Operation for Value ***** ***** ***** ***** ***** *****/ public void saveValue(String key, String value) { log.debug("Save string into redis, key:{}, value:{}", key, value); redisTemplate.opsForValue().set(key, value); } public void saveObject(String key, Object object) { log.debug("Save object into redis, key:{}, value:{}", key, object); redisTemplate.opsForValue().set(key, object); } public Object readObject(String key) { log.debug("Read object from redis, key:{}", key); return redisTemplate.opsForValue().get(key); } /***** ***** ***** ***** ***** ***** Operation for List ***** ***** ***** ***** ***** *****/ public void saveList(String key, List<?> list) { log.debug("Save a list into redis, key:{}", key); redisTemplate.delete(key); for (Object l : list) { redisTemplate.opsForList().rightPush(key, l); } } public List<?> readList(String key) { log.debug("Read a list from redis, key:{}", key); return redisTemplate.opsForList().range(key, 0, -1); } /** * 放入队列的最后一个位置 */ public void pushEnd(String key, Object value) { redisTemplate.opsForList().rightPush(key, value); } /** * 取队列的最后一个元素 */ public Object popEnd(String key) { return redisTemplate.opsForList().rightPop(key); } /** * 放入队列的第一个位置 */ public void pushFirst(String key, Object value) { redisTemplate.opsForList().leftPush(key, value); } /** * 取队列的第一个元素 */ public Object popFirst(String key) { return redisTemplate.opsForList().leftPop(key); } /***** ***** ***** ***** ***** ***** Operation for Map ***** ***** ***** ***** ***** *****/ public void saveMap(String key, Map<?, ?> map) { log.debug("Save a map into redis, key:{}", key); redisTemplate.delete(key); redisTemplate.opsForHash().putAll(key, map); } public void saveMapValue(String key, String hashKey, Object hashValue) { log.debug("Save map value into redis, key:{}, hashKey:{}", key, hashKey); redisTemplate.opsForHash().put(key, hashKey, hashValue); } public void deleteMapValue(String key, String hashKey) { log.debug("Delete map value from redis, key:{}, hashKey:{}", key, hashKey); redisTemplate.opsForHash().delete(key, hashKey); } public Map<?, ?> readMap(String key) { log.debug("Read a map from redis, key:{}", key); return redisTemplate.opsForHash().entries(key); } public Object readMapValue(String key, String hashKey) { log.debug("Read map value from redis, key:{}, hashKey:{}", key, hashKey); return redisTemplate.opsForHash().get(key, hashKey); } public Set<?> readMapKeys(String key) { log.debug("Read map keys from redis, key:{}", key); return redisTemplate.opsForHash().keys(key); } public List<?> readMapValues(String key) { log.debug("Read map values from redis, key:{}", key); return redisTemplate.opsForHash().values(key); } public boolean hasHashKey(String key, String hashKey) { return redisTemplate.opsForHash().hasKey(key, hashKey); } }
-
POJO 类如下:
package com.example.demo.domain; import lombok.Data; import java.io.Serializable; import java.util.HashSet; import java.util.Set; /** * @author smileorsilence */ @Data public class User implements Serializable { private Long id; private String username; private String password; private Set<Role> roles = new HashSet<>(); }
-
RedisServiceTest 测试类如下:
package com.example.demo.service; import com.example.demo.config.RedisConfiguration; import com.example.demo.domain.User; import com.example.demo.service.util.RedisKey; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author smileorsilence */ @Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RedisConfiguration.class, RedisService.class}) public class RedisServiceTest { @Autowired private RedisService redisService; @Test public void expireKey() throws Exception { } @Test public void saveMapValue() throws Exception { User user = new User(); user.setId(1L); user.setUsername("红楼隔雨相望冷"); user.setPassword("smileorsilence"); redisService.saveMapValue(RedisKey.SMILEORSILENCE_USER, user.getId().toString(), user); } @Test public void readMapValue() throws Exception { User user = new User(); user.setId(1L); user = (User) redisService.readMapValue(RedisKey.SMILEORSILENCE_USER, user.getId().toString()); log.info(">>>>> >>>>> user <<<<< <<<<< {}", user); } }
-
运行 saveMapValue 单元测试方法,测试通过,结果如下:
-
运行 readMapValue 单元测试方法,测试不通过,抛出异常如下:
-
抛出异常后修改 readMapValue 单元测试方法,使用 map 接收读取到的数据,方法如下:
@Test public void readMapValue() throws Exception { User user = new User(); user.setId(1L); Map<String, Object> userMap = (Map<String, Object>) redisService.readMapValue(RedisKey.SMILEORSILENCE_USER, user.getId().toString()); user.setUsername((String) userMap.get("username")); user.setPassword((String) userMap.get("password")); log.info(">>>>> >>>>> user <<<<< <<<<< {}", user); }
-
修改测试方法后测试通过,但这样操作十分繁琐,尤其是遇到对象嵌套情况的时候。刚开始思考是不是 redis 使用的序列化和反序列化方式的问题,于是将自定义的 JsonRedisSerializer 序列化类替换成 SpringData 自带的 Jackson2JsonRedisSerializer 序列化类,发现结果一样。后来记起以前做过一个项目有类似的情况但是并没有抛出类型转换异常,将两者对比后修改 RedisConfiguration 配置类如下:
package com.example.demo.config; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author smileorsilence */ @Configuration @EnableConfigurationProperties(RedisProperties.class) public class RedisConfiguration { @Autowired private RedisProperties redisProperties; @Bean public JedisConnectionFactory redisConnectionFactory() { JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory(); redisConnectionFactory.setHostName(redisProperties.getHost()); redisConnectionFactory.setPort(redisProperties.getPort()); redisConnectionFactory.setDatabase(redisProperties.getDatabase()); if (this.redisProperties.getPassword() != null) redisConnectionFactory.setPassword(redisProperties.getPassword()); if (this.redisProperties.getTimeout() > 0) redisConnectionFactory.setTimeout(redisProperties.getTimeout()); return redisConnectionFactory; } @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JsonRedisSerializer()); //for hash redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new JsonRedisSerializer()); // redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); return redisTemplate; } public static class JsonRedisSerializer implements RedisSerializer<Object> { private ObjectMapper objectMapper = new ObjectMapper(); public JsonRedisSerializer() { this.objectMapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); } @Override public byte[] serialize(Object o) throws SerializationException { if (o == null) { return new byte[0]; } else { try { return this.objectMapper.writeValueAsBytes(o); } catch (Exception e) { throw new SerializationException("Could not write JSON: " + e.getMessage()); } } } @Override public Object deserialize(byte[] bytes) throws SerializationException { if (bytes == null) { return null; } else { try { return this.objectMapper.readValue(bytes, Object.class); } catch (Exception e) { throw new SerializationException("Could not read JSON: " + e.getMessage()); } } } } }
与之前的配置类相比增加了两行代码:
this.objectMapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
-
再次运行 saveMapValue 和 readMapValue 单元测试方法,测试通过,结果如下:
-
参考有关的博客文章,发现这两行代码的作用是将 json 转换成 java 对象,如果不设置则会默认将 json 转换成 hashmap,另外以上两行代码替换成如下代码后也可以通过测试:
this.objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); this.objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
只不过在Redis中存储的数据内容与前者稍有不同: