Spring Boot:通過spring-boot-starter-data-redis源碼瞭解starter和autoconfigure模塊

注:本文Spring Boot爲2.X版本
在Spring Boot中,官方提供了spring-boot-autoconfigure包和starter包用來幫助我們簡化配置,比如之前要建一個Spring mvc項目,需要我們配置web.xml,dispatcherservlet-servlet.xml,applicationContext.xml等等。而在Spring Boot中只需要在pom中引入

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

就能完成之前所有的工作了。簡直so easy啊。
但是隻會用是不行的,還要知其所以然,本文以官方的starter:spring-boot-starter-data-redis爲例,從源碼層面上分析整個自動化配置的過程。以期對starter和autoconfigure這兩個Spring Boot的核心模塊進行梳理。

在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來處理緩存的相關操作了。

一.RedisTemplate的注入

讓我們先來看下RedisTemplate是如何被注入的。

1.RedisProperties

application.properties中ctrl+左擊redis的相關配置項,會打開spring-boot-autoconfigure\2.0.2.RELEASE\spring-boot-autoconfigure-2.0.2.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:[email protected]: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();
	......
}

(1) @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註解的方法,這些方法將會被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext類進行掃描,並用於構建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 List<JedisClientConfigurationBuilderCustomizer> builderCustomizers;
	JedisConnectionConfiguration(RedisProperties properties,
			ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
			ObjectProvider<RedisClusterConfiguration> clusterConfiguration,
			ObjectProvider<List<JedisClientConfigurationBuilderCustomizer>> builderCustomizers) {
		super(properties, sentinelConfiguration, clusterConfiguration);
		this.properties = properties;
		this.builderCustomizers = builderCustomizers
				.getIfAvailable(Collections::emptyList);
	}
	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
		return createJedisConnectionFactory();
	}
	......
}
  • @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方法

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

終於找到注入redisTemplate的地方了= =。

  • 這是個被@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了,不能再注入第二個)。

這個我們可以做個小測試
新建一個Spring Boot項目,勾一個web即可。
新建BC類。
BC類用來模擬LettuceConnectionConfigurationJedisConnectionConfiguration
這裏類上沒有添加@Configuration註解也是爲了不被Spring掃描到,然後通過@Import纔會進行注入。

public class B{
	@Bean
	@ConditionalOnMissingBean(TestInfoA.class)
	public TestInfoB testInfoA() {
		return new TestInfoB();
	}
}

public class C{
	@Bean
	@ConditionalOnMissingBean(TestInfoA.class)
	public TestInfoC testInfoA() {
		return new TestInfoC();
	}
}

新建
TestInfoATestInfoBTestInfoC。其中TestInfoA爲接口,TestInfoBTestInfoC都實現了TestInfoA。用來模擬RedisConnectionFactory接口,JedisConnectionFactoryLettuceConnectionFactory

public interface TestInfoA {
}

public class TestInfoB implements TestInfoA{
}

public class TestInfoC implements TestInfoA{
}

新建TestInfo,模擬RedisTemplate

public class TestInfo {
	private TestInfoA info;

	public TestInfoA getInfo() {
		return info;
	}

	public void setInfo(TestInfoA info) {
		this.info = info;
	}
}

新建TestConfig,用來模擬RedisAutoConfiguration

@Configuration
@Import({B.class,C.class})
public class TestConfig {
	@Bean
	@ConditionalOnMissingBean(name = "testInfo")
	public TestInfo testInfo(TestInfoA param) {
		TestInfo info = new TestInfo();
		info.setInfo(param);
		return info;
	}
}

爲了更好的展示,所以這裏的結構完全仿照redis-starter的,實際上也不用那麼複雜就是了,然後在TestConfig中的testInfo方法中打個斷點,爲了看TestInfoA實際上是TestInfoB還是TestInfoC類,運行。
在這裏插入圖片描述
可以看到實際上是TestInfoB,而@import中也是B在前。
然後改成@Import({C.class,B.class})
然後結果
在這裏插入圖片描述
可以看出是按照出現在@Improt中的順序來注入的。
然後測試下把B,C類的@ConditionalOnMissingBean(TestInfoA.class)註釋掉
報錯

The bean ‘testInfoA’, defined in class path resource
[com/my/startingProcedure/my/C.class], could not be registered. A bean
with that name has already been defined in class path resource
[com/my/startingProcedure/my/B.class] and overriding is disabled.

總結:redisTemplate方法中的RedisConnectionFactory其實是LettuceConnectionFactory
然後我們就可以通過這個注入的RedisTemplate來操作redis了。

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
在這裏插入圖片描述
在這裏插入圖片描述
EnableAutoConfiguration是不是和剛纔講到的註解一模一樣呢?_(:з」∠*)_

這裏還涉及到了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);階段完成配置類的解析、各種BeanFactoryPostProcessor和BeanPostProcessor的註冊、國際化配置的初始化、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>

然後ctrl+左鍵繼續查看
在這裏插入圖片描述
能看到版本了,再繼續查看。
在這裏插入圖片描述
在這裏插入圖片描述
發現其會引入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了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章