本文共 6548 字,大约阅读时间需要 21 分钟。
背景:
今天测试redis自定义配置时出现了连接空指针的问题,并且同样代码在不同版本下表现不同,让我们来结合源码详细分析下问题所在。起初我们SpringBoot使用的是1.5.9
版本,在自定义RedisTemplate各种参数配置时出现了问题:
@Bean(name = "foreRedisTemplate") public RedisTemplate getForeRedisTemplate(){ JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMinIdle(minIdle); jedisPoolConfig.setMaxWaitMillis(maxWait); JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); connectionFactory.setPoolConfig(jedisPoolConfig); connectionFactory.setDatabase(foreDatabase); connectionFactory.setHostName(host); connectionFactory.setPassword(password); connectionFactory.setPort(port); connectionFactory.setTimeout(timeout); StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(connectionFactory); return stringRedisTemplate; }
运行测试后报错如下:
Cannot get Jedis connection; nested exception is java.lang.NullPointerException[2019-02-17 01:10:17.943] ERROR [http-nio-8080-exec-1] DirectJDKLog.java:181 - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is java.lang.NullPointerException] with root causejava.lang.NullPointerException: null at redis.clients.jedis.BinaryJedis.(BinaryJedis.java:101) at redis.clients.jedis.Jedis. (Jedis.java:78) at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:197) at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348) at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129) at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92) at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169) at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:91) at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:169)
可能熟悉上面配置代码的同学发现了问题所在,貌似缺少了一步connectionFactory.afterPropertiesSet()
?
但是我保持代码不变,将springboot版本切换到2.0.0
,居然又没有报错,这是为啥?
题外话:
我在切换版本时发现一个有意思的改动: 1.5.9版本的 spring-boot-starter-data-redis maven依赖是包含jedis
的,但到了2.0.0版本,却不包含了,需要我们手动添加jedis依赖: 1.5.9只需要添加:
org.springframework.boot spring-boot-starter-data-redis
2.0.0需要添加两个:
org.springframework.boot spring-boot-starter-data-redis redis.clients jedis 2.9.0
原来SpringBoot2.X默认采用lettuce
,而1.5默认采用的是jdeis
;Lettuce和Jedis都是连接Redis Server的客户端程序,Jedis在实现上是直连redis server,多线程环境下非线程安全,除非使用连接池,为每个Jedis实例增加物理连接。Lettuce基于Netty的实例连接,可以再多个线程间并发访问,且线程安全,满足多线程环境下的并发访问,同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
我们通过报错信息可以大概知道,JedisConnectionFactory 在 getConnection 时 fetchJedisConnector方法产生了空指针错误,下面我们来分析JedisConnectionFactory源码,定位问题源头。
先看1.5.9版本的源码:
进入fetchJedisConnector方法:protected Jedis fetchJedisConnector() { try { if (usePool && pool != null) { return pool.getResource(); } Jedis jedis = new Jedis(getShardInfo()); // force initialization (see Jedis issue #82) jedis.connect(); potentiallySetClientName(jedis); return jedis; } catch (Exception ex) { throw new RedisConnectionFailureException("Cannot get Jedis connection", ex); }}
该方法先判断是否使用并且存在pool,如果不满足条件则new一个Jedis对象,并传入 getShardInfo() 作为构造参数,继续看该方法以及Jedis构造函数:
// 获取JedisConnectionFactory类的shardInfo属性public JedisShardInfo getShardInfo() { return shardInfo;}
public Jedis(JedisShardInfo shardInfo) { super(shardInfo);}
// BinaryJedis类为Jedis父类public BinaryJedis(final JedisShardInfo shardInfo) { client = new Client(shardInfo.getHost(), shardInfo.getPort(), shardInfo.getSsl(), shardInfo.getSslSocketFactory(), shardInfo.getSslParameters(), shardInfo.getHostnameVerifier()); client.setConnectionTimeout(shardInfo.getConnectionTimeout()); client.setSoTimeout(shardInfo.getSoTimeout()); client.setPassword(shardInfo.getPassword()); client.setDb(shardInfo.getDb());}
可以看到BinaryJedis类中使用了大量shardInfo对象的方法,那么问题来了,shardInfo会为空吗?
我们观察整个JedisConnectionFactory类,发现只有一个地方给shardInfo对象初始化,那就是 afterPropertiesSet 方法!
public void afterPropertiesSet() { // 此处真正给shardInfo赋值 if (shardInfo == null) { shardInfo = new JedisShardInfo(hostName, port); if (StringUtils.hasLength(password)) { shardInfo.setPassword(password); } if (timeout > 0) { setTimeoutOn(shardInfo, timeout); } } if (usePool && clusterConfig == null) { this.pool = createPool(); } if (clusterConfig != null) { this.cluster = createCluster(); } }
可以看到,该方法不仅保证了shardInfo 不为空,还创建了pool对象(非cluster模式下)(注意该类的usePool 属性默认值就是true),后面的jedis连接都是从pool里获取资源了!
所以报错原因就是没有调用afterPropertiesSet方法,导致shardInfo对象为空,之后调用shardInfo.getHost()等就报错空指针了!
那为什么2.0.0版本不加afterPropertiesSet方法没事呢?
我们再看看2.0.0版本的源码:
protected Jedis fetchJedisConnector() { try { if (getUsePool() && pool != null) { return pool.getResource(); } // 此处注意了,和1.5.9版本不同 Jedis jedis = createJedis(); // force initialization (see Jedis issue #82) jedis.connect(); potentiallySetClientName(jedis); return jedis; } catch (Exception ex) { throw new RedisConnectionFailureException("Cannot get Jedis connection", ex); }}
可以看到,1.5.9版本是new Jedis(getShardInfo())
来创建jedis对象,但这里是调用createJedis()
方法来创建的,看看这个方法:
private Jedis createJedis() { // 该属性默认为false if (providedShardInfo) { return new Jedis(getShardInfo()); } Jedis jedis = new Jedis(getHostName(), getPort(), getConnectTimeout(), getReadTimeout(), isUseSsl(), clientConfiguration.getSslSocketFactory().orElse(null), // clientConfiguration.getSslParameters().orElse(null), // clientConfiguration.getHostnameVerifier().orElse(null)); Client client = jedis.getClient(); getRedisPassword().map(String::new).ifPresent(client::setPassword); client.setDb(getDatabase()); return jedis;}
重点来了,这里创建jedis对象先做了一步判断providedShardInfo
,相当于非空校验,不像之前那样直接就干。。下面就是中规中矩的new Jedis()创建对象了;
看到这里,相信大家已经明白了开篇的问题究竟为何如此了;那么问题来了,我们到底需要加上afterPropertiesSet方法吗?
答案是肯定的,调用该方法才可以使我们配置的各种参数生效,比如pool、cluster等。转载地址:http://orlfb.baihongyu.com/