Springboot+Redis序列化问题解决方案

博主在Spring Boot集成Redis时遇到问题,如在Redis客户端设置值,代码获取为null,用Jedis插入值,客户端获取为(nil)。原因是Spring默认使用JDK序列化。文章分析了多种序列化方式,并给出解决方案,如注入序列化器、修改配置类等。

背景:

啊~今天的杭州在经历昨天星期六一整天的雨过后,终于放晴了,所以心情好,撸一段Springboot集成Redis的代码,并且在新电脑上安装Redis玩一玩时发现的一个坑,哈哈~开心,又学习到了东西。


问题:

1、安装完Redis后,在redis客户端,进行了set key value,然后用代码去获取,居然为null。

2、用jedis成功的将key-value插入到redis中,在redis客户端get key居然为(nil)。


问题复现:

1、在redis客户端设置key-value,如下图:

2、在代码中用jedis去获取key为key_one的值,代码如下:

package com.pyy.helloWorld;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedisTemplate {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testString(){
//        redisTemplate.opsForValue().set("ccc","hello");
        System.out.println("value = "+redisTemplate.opsForValue().get("key_one"));
//        Assert.assertEquals("hello",redisTemplate.opsForValue().get("ccc"));
    }
}

结果输出: null

 

3、我们在代码中设置key-value:

4、我们返回客户端去获取:

 


原因分析:

这里,我们一定非常郁闷,what?怎么会这样,我们首先用命令 keys * 查看所以的key来看一看,通过jedis操作的key-value到底有没有入库。

嘿嘿,这里我们发现了,redis中确实存在另一个key为 "\xac\xed\x00\x05t\x00\akey_two" 这个玩意。

虽然springboot提供了redis服务,但是set的过程存在序列化的问题,我们set到Redis服务器中的key时这样的:\xac\xed\x00\x05t\x00\a  。

当数据存储到Redis中的时候,key和value都是通过spring serializer进行序列化的,RedisTemplate,spring默认会使用jdk序列化。

StringRedisTemplate默认是使用StringSerializer。

同时,SpringData还提供了其他的序列化方式,如下:

 

GenericToStringSerializer:使用Spring转换服务进行序列化;
JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON;
Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON;
JdkSerializationRedisSerializer:使用Java序列化;
OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用于XML序列化;
StringRedisSerializer:序列化String类型的key和value。实际上是String和byte数组之间的转换。

 


解决方案:

我们直接注入官方给定的keySerializer,valueSerializer,hashKeySerializer即可,那么使用注解的话我们需要自己编写RedisCacheConfig配置类,

缓存主要有几个要实现的类:

                1、CacheManager缓存管理器;

                2、具体操作实现类;

                3、CacheManager工厂类(这个可以使用配置文件配置的进行注入,也可以通过编码的方式进行实现);

                4、缓存key生产策略(当然Spring自带生成策略,但是在Redis客户端进行查看的话是系列化的key,对于我们肉眼来说就是感觉是乱码了,这里我们先使用自带的缓存策略)。

代码如下:

 

package com.pyy.helloWorld.configuration;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 自定义key. 这个可以不用
     * 此方法将会根据类名+方法名+所有参数的值生成唯一的一个key,即使@Cacheable中的value属性一样,key也会不一样。
     */
    @Bean
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }


    /**
     * 定制redisTemplet 作用 存储到redis中的对象以序列化的方式
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //定义value序列化方式
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());//定义key序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return  redisTemplate;
    }

    /**
     * redis缓存序列化
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        RedisCacheConfiguration config =  RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheConfiguration redisCacheConfiguration1 = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration1)
                .build();
        return  redisCacheManager;
    }



//    @Bean
//    public CacheManager cacheManager(RedisTemplate redisTemplate) {
//        RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
//        //设置缓存过期时间
//        //rcm.setDefaultExpiration(60);//秒
//        return rcm;
//    }
}

 

我么用代码设置key-value:

用redis客户端去查询:

 

但是,当我们直接用Redis客户端去设置一个key_four时:

再用jedis代码去进行查询操作,就会报错:

 

报错信息如下:

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unrecognized token 'hello4': was expecting ('true', 'false' or 'null')
 at [Source: (byte[])"hello4"; line: 1, column: 13]; nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'hello4': was expecting ('true', 'false' or 'null')
 at [Source: (byte[])"hello4"; line: 1, column: 13]

	at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:75)
	at org.springframework.data.redis.core.AbstractOperations.deserializeValue(AbstractOperations.java:334)
	at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184)
	at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:95)
	at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53)
	at com.pyy.helloWorld.TestRedisTemplate.testString(TestRedisTemplate.java:21)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'hello4': was expecting ('true', 'false' or 'null')
 at [Source: (byte[])"hello4"; line: 1, column: 13]
	at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1804)
	at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:679)
	at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._reportInvalidToken(UTF8StreamJsonParser.java:3524)
	at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._handleUnexpectedValue(UTF8StreamJsonParser.java:2619)
	at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._nextTokenNotInObject(UTF8StreamJsonParser.java:824)
	at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:721)
	at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4141)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4000)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3129)
	at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:73)
	... 37 more

分析报错,意思就是Jaackson的序列化无法解析“hello4”,我们只需要讲value的序列化方式改成String的就行,如下:

 

这样,问题就解决了,是不是很嗨皮!!!

 


感谢各位小伙伴们的阅读,由于个人能力有限,如果本文中有错误,希望大家批评指正,一起进步!

代码欢迎访问github上拉去:Hello_SpringBoot

本文借鉴博客:

【springBoot】springBoot集成redis的key,value序列化的相关问题

  springboot+redis 缓存解决数据乱码问题

  【坑】Springboot+Redis序列化坑

 

 

### 使用 Spring Boot、Redis 和 JWT 实现身份验证 在现代 Web 应用程序开发中,使用 JWT(JSON Web Token)作为无状态的身份验证机制是一种常见的方式。为了增强系统的安全性并支持分布式部署场景下的会话管理,可以结合 Redis 来存储和管理 JWT 的相关信息。 以下是基于 Spring Boot 3 + Spring Security 6 + JWT + Redis 的实现方案: #### 1. **项目依赖** 首先,在 `pom.xml` 文件中添加必要的依赖项: ```xml <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <scope>runtime</scope> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <scope>runtime</scope> <version>0.11.5</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- MySQL and MyBatis (Optional, if using database for user management) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> </dependencies> ``` --- #### 2. **JWT 工具类** 创建一个工具类用于生成和解析 JWT Token。 ```java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; public class JwtUtil { private static final String SECRET_KEY = "your_secret_key"; // 密钥需安全保存 public String generateToken(String username) { return Jwts.builder() .setSubject(username) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } } ``` --- #### 3. **Redis 配置** 通过 Redis 缓存用户的 Token 及其有效期。 ```java @Configuration public class RedisConfig { @Bean public LettuceConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(); // 默认连接本地 Redis } @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // JSON序列化 return template; } } ``` --- #### 4. **登录逻辑** 当用户成功登录后,生成 JWT 并将其存储到 Redis 中。 ```java @RestController @RequestMapping("/auth") public class AuthController { @Autowired private UserService userService; // 假设有一个服务来处理用户数据 @Autowired private JwtUtil jwtUtil; @Autowired private RedisTemplate<String, Object> redisTemplate; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { User user = userService.authenticate(request.getUsername(), request.getPassword()); if (user != null) { String token = jwtUtil.generateToken(user.getUsername()); // 将 Token 存入 Redis,设置过期时间为 1 天 redisTemplate.opsForValue().set("AUTH:" + token, user.getId(), Duration.ofDays(1)); return ResponseEntity.ok(Map.of("token", token)); } else { throw new RuntimeException("Invalid credentials"); } } } ``` --- #### 5. **拦截器与过滤器** 在请求到达控制器之前,检查 Token 是否有效以及是否存在 Redis 中。 ```java @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private RedisTemplate<String, Object> redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { try { String token = authHeader.substring(7); // 提取 Token // 检查 Token 是否存在于 Redis 中 Boolean existsInRedis = redisTemplate.hasKey("AUTH:" + token); if (!existsInRedis) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } // 解析 Token 获取用户名 Claims claims = jwtUtil.parseToken(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( claims.getSubject(), null, Collections.emptyList() ); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } } filterChain.doFilter(request, response); } } ``` --- #### 6. **Spring Security 配置** 配置 Spring Security 以启用自定义过滤器。 ```java @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests() .requestMatchers("/auth/login").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } } ``` --- ### 总结 上述方法展示了如何利用 Spring Boot、Redis 和 JWT 构建一套完整的身份验证解决方案[^1]。该方案不仅实现了无状态的 Token 认证,还借助 Redis 提供了高效的 Token 管理能力[^4]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值