背景:
今天测试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 cause
java.lang.NullPointerException: null
at redis.clients.jedis.BinaryJedis.<init>(BinaryJedis.java:101)
at redis.clients.jedis.Jedis.<init>(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只需要添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.0.0需要添加两个:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
原来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等。