Skip to content

Latest commit

 

History

History
377 lines (348 loc) · 19.7 KB

1.spring-boot-starter-data-redis.md

File metadata and controls

377 lines (348 loc) · 19.7 KB

通过spring-boot-starter-data-redis源码了解starter和autoconfigure模块

在Spring Boot中,官方提供了spring-boot-autoconfigure包和starter包用来帮助我们简化配置,Spring Boot中只需要在pom中引入。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

就能完成之前所有的工作了。简直so easy啊。基本用法参考 但是只会用是不行的,还要知其所以然,本文以官方的starter:spring-boot-starter-data-redis为例,从源码层面上分析整个自动化配置的过程。以及对starterautoconfigure这两个Spring Boot的核心模块进行梳理。 了解原理后,我会通过模拟spring-boot-starter-data-redis,并使用Jedis来创建一个处理redis的自定义starter:my-redis-starter。最后会详细说明自定义starter的创建过程。

在Spring Boot中使用默认的redis客户端只需要

  • 在pom.xml中引入
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后在application.properties中配置ip,密码等必要参数

spring.redis.host=106.15.108.50
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
#等等

就可以直接在我们的业务中调用org.springframework.data.redis.core.RedisTemplate来处理缓存的相关操作了。

1.RedisTemplate的注入

让我们先来看下RedisTemplate是如何被注入的。

RedisProperties

application.propertiesctrl+左击redis的相关配置项,会打开spring-boot-autoconfigure\2.1.7.RELEASE\spring-boot-autoconfigure-2.1.7.RELEASE.jar中的RedisProperties。 打开org.springframework.boot.autoconfigure.data.redis.RedisProperties.class

@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {

	/**
	 * Database index used by the connection factory.
	 */
	private int database = 0;

	/**
	 * Connection URL. Overrides host, port, and password. User is ignored. Example:
	 * redis://user:password@example.com:6379
	 */
	private String url;

	/**
	 * Redis server host.
	 */
	private String host = "localhost";

	/**
	 * Login password of the redis server.
	 */
	private String password;

	/**
	 * Redis server port.
	 */
	private int port = 6379;

	/**
	 * Whether to enable SSL support.
	 */
	private boolean ssl;

	/**
	 * Connection timeout.
	 */
	private Duration timeout;

	private Sentinel sentinel;

	private Cluster cluster;

	private final Jedis jedis = new Jedis();

	private final Lettuce lettuce = new Lettuce();
    //more
  • @ConfigurationProperties(prefix = "spring.redis") 设置绑定属性的前缀,然后看下前面的一些属性,是不是很眼熟?前缀+属性名就是之前在application.properties中配置的,如果我们没有配置端口这种属性,那么这里也会提供部分默认配置。 当然,只是这些是没办法让Spring Boot在启动时扫描到该类的,所以需要下一个类org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.class。 然后我们还能找到
private final Jedis jedis = new Jedis();

private final Lettuce lettuce = new Lettuce();

一般用Java操作redis用的较多几个Java客户端为Jedis,Redisson,Lettuce。这里可知官方提供的spring-boot-starter-data-redis底层是用Jedis/Lettuce实现的,知道了这个我们也能够借鉴这个starter来使用其他的客户端来实现了。

2.RedisAutoConfiguration

打开org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.class

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

}
  • (1)@Configuration常见的配置注解,内部含有一个以上的@Bean,让Spring能扫描到内部的@Bean,当然在Spring Boot中,默认只会扫描到启动类所在包或其下级包的类,所以还会通过其他的设置来让这个类被扫描到,这个后面会详细说明。

@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContextAnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。

  • (2)@ConditionalOnClass(RedisOperations.class),当存在RedisOperations类时才会进行扫描,这个类什么时候被引入classpath的之后会提到。
  • (3)@EnableConfigurationProperties(RedisProperties.class)RedisProperties 类被扫描到的关键。这时,如果RedisAutoConfiguration被扫描到,则同时也会去扫描RedisProperties类。
  • (4)@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })通过@Import注解方式生成类实例并注入Spring容器。

@Import注解通过导入的方式实现把实例加入springIOC容器中

让我们打开JedisConnectionConfiguration简要看下

@Configuration
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
class JedisConnectionConfiguration extends RedisConnectionConfiguration {

	private final RedisProperties properties;

	private final ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers;

	JedisConnectionConfiguration(RedisProperties properties,
			ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
			ObjectProvider<RedisClusterConfiguration> clusterConfiguration,
			ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
		super(properties, sentinelConfiguration, clusterConfiguration);
		this.properties = properties;
		this.builderCustomizers = builderCustomizers;
	}

	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
		return createJedisConnectionFactory();
	}

	private JedisConnectionFactory createJedisConnectionFactory() {
		JedisClientConfiguration clientConfiguration = getJedisClientConfiguration();
		if (getSentinelConfig() != null) {
			return new JedisConnectionFactory(getSentinelConfig(), clientConfiguration);
		}
		if (getClusterConfiguration() != null) {
			return new JedisConnectionFactory(getClusterConfiguration(), clientConfiguration);
		}
		return new JedisConnectionFactory(getStandaloneConfig(), clientConfiguration);
	}
    // more
  • @Import注解会通过JedisConnectionConfiguration构造方法将JedisConnectionConfiguration的实例注入到Spring容器中,这里有一个RedisProperties参数,实际上就是在(3)中注入的RedisProperties,这样JedisConnectionConfiguration就获得了RedisProperties(在初始化方法里面赋值),也就获得了之前我们在application.propertie中配置的redis服务器连接属性。
  • 通过@Configuration@Bean的定义可知,会扫描到redisConnectionFactory()方法并返回实体,并注入到Spring容器,对应的类为RedisConnectionFactory。(JedisConnectionConfiguration实现了RedisConnectionFactory接口,所以可以这样)
@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
		return createJedisConnectionFactory();
	}

	private JedisConnectionFactory createJedisConnectionFactory() {
		JedisClientConfiguration clientConfiguration = getJedisClientConfiguration();
		if (getSentinelConfig() != null) {
			return new JedisConnectionFactory(getSentinelConfig(), clientConfiguration);
		}
		if (getClusterConfiguration() != null) {
			return new JedisConnectionFactory(getClusterConfiguration(), clientConfiguration);
		}
		return new JedisConnectionFactory(getStandaloneConfig(), clientConfiguration);
	}

	private JedisClientConfiguration getJedisClientConfiguration() {
		JedisClientConfigurationBuilder builder = applyProperties(JedisClientConfiguration.builder());
		RedisProperties.Pool pool = this.properties.getJedis().getPool();
		if (pool != null) {
			applyPooling(pool, builder);
		}
		if (StringUtils.hasText(this.properties.getUrl())) {
			customizeConfigurationFromUrl(builder);
		}
		customize(builder);
		return builder.build();
	}
  • getJedisClientConfiguration()方法,该方法从之前注入的RedisProperties中获取了 Jedis客户端连接池。
  • createJedisConnectionFactory会根据配置的redis参数判断用单机/哨兵/集群模式来创建JedisConnectionFactory实例。

总结:创建并注入了JedisConnectionFactory实例,JedisConnectionFactory实例中包含有Jedis的客户端连接池,之后就能用其创建连接了。 -(5)redisTemplate方法(RedisAutoConfiguration)

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
		RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
	RedisTemplate<Object, Object> template = new RedisTemplate<>();
	template.setConnectionFactory(redisConnectionFactory);
	return template;
}

这是个被@Bean注解的方法,因此会被Spring扫描并注入。

  • @ConditionalOnMissingBean(name = "redisTemplate")当Spring容器中不存在RedisTemplate实例时才会进行扫描注入,很明显是为了防止重复注入。
  • 该方法有一个RedisConnectionFactory参数。 而我们知道(4)中redisConnectionFactory方法最后会注入一个JedisConnectionFactory实例,而JedisConnectionFactory又是继承于RedisConnectionFactory。同志们,你们懂我的意思了吧∠( ᐛ 」∠)_

总结:该方法会将先前注入的redisConnectionFactory赋给新建的redisTemplate实例, 然后将redisTemplate实例注入Spring容器。

但是这里出现一个问题了 开始时通过@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })注入了Lettuce和Jedis两个连接配置实例, 而这两个中又都已@Bean的形式注入了JedisConnectionFactoryLettuceConnectionFactory两个实例(这两个实例的类又都是继承于 RedisConnectionFactory的),并且注入时都是对应RedisConnectionFactory类的。那么redisTemplate方法最后是使用哪个实例来创建RedisTemplate的呢? 通过debug我们知道实际用的是LettuceConnectionFactory实例。 这么看是按照@Import中的排序来的, 这里LettuceConnectionConfiguration在前,所以会先扫描LettuceConnectionConfiguration。相关代码

Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	public LettuceConnectionFactory redisConnectionFactory(
			ClientResources clientResources) throws UnknownHostException {
		LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(
				clientResources, this.properties.getLettuce().getPool());
		return createLettuceConnectionFactory(clientConfig);
	}

LettuceConnectionConfiguration中会创建LettuceConnectionFactory实例,并将其注入为redisConnectionFactory类的实例, 然后在JedisConnectionConfiguration中的类似代码:

@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
		return createJedisConnectionFactory();
	}

也会创建一个JedisConnectionFactory 实例,并将其注入为redisConnectionFactory类的实例。 双方都有@ConditionalOnMissingBean(RedisConnectionFactory.class)约束,所以当LettuceConnectionConfigurationRedisConnectionFactory类被注入了对应的实例后,JedisConnectionConfiguration对应的代码就不会再执行了,所以最后RedisConnectionFactory类的实例实际上是LettuceConnectionFactory。 只要把@Import中的顺序换一下就能改变RedisConnectionFactory类的实例了。 可能有的童鞋会问,“如果把@ConditionalOnMissingBean(RedisConnectionFactory.class)去掉呢?” 这样的话JedisConnectionConfiguration中的@Bean是否能覆盖掉之前的那个,实现重复注入呢?抱歉,这样会报错(大致意思是RedisConnectionFactory已经有一个对应的bean了,不能再注入第二个)。

3. spring-boot-autoconfigure

Spring Boot可以依据classpath里面的依赖内容来自动配置bean到IOC容器。 但是要开启这个自动配置功能需要添加@EnableAutoConfiguration注解。 上面指的自动配置功能事实上就是spring-boot-autoconfigure模块 然后让我们打开一个Spring Boot项目的启动项,是否注意到有一个@SpringBootApplication注解,这个是默认就有的。然后点开

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

发现@EnableAutoConfiguration,也就是说Spring Boot是默认开启自动配置功能的,即spring-boot-autoconfigure模块是被默认引用的。 然后让我们看下spring-boot-autoconfigure.jar中的该文件 相信能够找到RedisAutoConfiguration org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\

  • 这里还涉及到了Spring Boot的启动过程
public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

在Spring Boot启动时,会在refreshContext(context);阶段完成配置类的解析、各种BeanFactoryPostProcessorBeanPostProcessor的注册、国际化配置的初始化、web内置容器的构造等等,这时会读取pom中引入jar的配置文件/META-INF/spring.factories,所以这里EnableAutoConfiguration下的所有类都会被实例化并注入Spring容器。 所以RedisAutoConfiguration就被扫描到了。 再来回顾下RedisAutoConfiguration

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
}

会发现@ConditionalOnClass(RedisOperations.class),如果想要被扫描到还需要在classpath中存在RedisProperties.class,这个又在哪呢? 点开RedisProperties.class,会发现其存在于spring-data-redis-2.1.3.RELEASE.jar中 但我们貌似没有引入spring-data-redis,这个是哪里来的呢?先让我们先看下之前pom中的引入,

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

能看到版本了,再继续查看。

 <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-redis</artifactId>
      <version>2.1.10.RELEASE</version>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <artifactId>jcl-over-slf4j</artifactId>
          <groupId>org.slf4j</groupId>
        </exclusion>
      </exclusions>
    </dependency>

发现其会引入spring-data-redis,因此只有当我们在pom中引入spring-boot-starter-data-redis时,RedisAutoConfiguration才会真正的开启扫描。这也体现了Spring Boot的即插即用和方便快捷的自动配置。 然后下面还有一个io.lettuce,而之前在RedisAutoConfiguration中我们知道redisTemplate方法最终会把一个LettuceConnectionFactory实例注入Spring容器,而在这里实际上就已经大致表明了RedisAutoConfiguration会使用Lettuce客户端了。

4.总结

当要使用Spring Boot提供的redis客户端功能时,注入RedisTemplate的流程大致如下。 1.pom中引入spring-boot-starter-data-redis,并配置application.properties。 2.pom会根据spring-boot-starter-data-redis来引入spring-data-redis。 3.spring-data-redis中包含RedisOperations类。 4.启动Spring Boot,在refreshContext(context);中会初始化beanFactory,读取配置信息,初始化Spring容器,注入bean。因为@EnableAutoConfiguration开启的关系,会读取配置中EnableAutoConfiguration相关的类,并实例化注入Spring 容器。 5.根据配置文件扫描到RedisAutoConfiguration。当RedisOperations存在时RedisAutoConfiguration才会被扫描。 6.通过@EnableConfigurationProperties(RedisProperties.class)@ConfigurationProperties(prefix = "spring.redis"),把application.properties中的对应属性进行绑定,并注入RedisProperties配置类。 7.RedisAutoConfiguration中的@Import会引入LettuceConnectionConfigurationJedisConnectionConfiguration 8.LettuceConnectionConfigurationJedisConnectionConfiguration被扫描,扫描到内部的@Bean,使用上一步中注入的RedisProperties bean作为参数来实例化LettuceConnectionFactoryJedisConnectionFactory,并以RedisConnectionFactory类注入Spring容器。 8.扫描并注入RedisAutoConfiguration类内的@Bean,其中会使用RedisConnectionFactory bean作参数实例化RedisTemplate。 9.将RedisTemplate实例注入。 10.然后就能通过引用RedisTemplate来操作redis了。