Spring Boot系列(二):Spring Boot自动装配原理解析

时间:2023-03-08 19:30:12

一、Spring Boot整合第三方组件(Redis为例)

  1、加依赖

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

  2、加配置

spring.redis.host=127.0.0.1
spring.redis.password=
spring.redis.port=6379
spring.redis.jedis.pool.max-idle=200
spring.redis.jedis.pool.max-active=1024
spring.redis.jedis.pool.max-wait=1000

  3、加注解(看各自的组件需要,比如整合Mybatis就需要,Redis不需要)

二、Spring Boot自动装配组件原理

  1、@SpringBootApplication注解

Spring Boot系列(二):Spring Boot自动装配原理解析

  2、AutoConfigurationImportSelector分析

  ① selectImports方法:

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
     //获取自动装配的入口
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

  ② getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata)方法:

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
    /**
* 获取候选的配置类,主要是到classpath下面的\META-INF\spring.factories中,
* 取key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的配置类
*/
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
/**去除重复的配置类,若我们自己写的starter 可能存主重复的*/
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
/**根据maven依赖导入的启动器过滤出需要导入的配置类*/
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

  ③ getCandidateConfigurations(annotationMetadata, attributes)方法:

/**
* Return the auto-configuration class names that should be considered. By default
* this method will load candidates using {@link SpringFactoriesLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}.
* @param metadata the source metadata
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
* attributes}
* @return a list of candidate configurations
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//去spring.factories中去查询EnableAutoConfiguration类
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

  ④ SpringFactoriesLoader.loadFactoryNames方法:

/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* @param factoryClass the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
//去spring.factories 中去查询EnableAutoConfiguration类
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

  ⑤ loadSpringFactories(classLoader)方法:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
} try {
/**
* The location to look for factories. Can be present in multiple JAR files.
* FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
*/
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

  spring.factories如下:

Spring Boot系列(二):Spring Boot自动装配原理解析

  3、RedisAutoConfiguration分析

  导入了三个组件:RedisTemplate,StringRedisTemplate,JedisConnectionConfiguration

Spring Boot系列(二):Spring Boot自动装配原理解析

  ① RedisTemplate组件(默认采用java序列化,所以一般要自定义该组件):

@Bean
//当没有Spring容器中没有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
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
template.setConnectionFactory(redisConnectionFactory);
return template;
}

  ② StringRedisTemplate(默认采用java序列化,所以一般要自定义该组件):

@Bean
//当没有Spring容器中没有StringRedisTemplate类型的Bean的时候才加载
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

  ③ JedisConnectionConfiguration组件:

/**
* Redis connection configuration using Jedis.
*/
@Configuration
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
class JedisConnectionConfiguration extends RedisConnectionConfiguration { /**
* redis配置
*/
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;
} /**
* Jedis连接工厂
* @return
* @throws UnknownHostException
*/
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
return createJedisConnectionFactory();
} /**
* Jedis连接工厂
* @return
*/
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();
} private JedisClientConfigurationBuilder applyProperties(JedisClientConfigurationBuilder builder) {
if (this.properties.isSsl()) {
builder.useSsl();
}
if (this.properties.getTimeout() != null) {
Duration timeout = this.properties.getTimeout();
builder.readTimeout(timeout).connectTimeout(timeout);
}
return builder;
} private void applyPooling(RedisProperties.Pool pool,
JedisClientConfiguration.JedisClientConfigurationBuilder builder) {
builder.usePooling().poolConfig(jedisPoolConfig(pool));
} private JedisPoolConfig jedisPoolConfig(RedisProperties.Pool pool) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(pool.getMaxActive());
config.setMaxIdle(pool.getMaxIdle());
config.setMinIdle(pool.getMinIdle());
if (pool.getTimeBetweenEvictionRuns() != null) {
config.setTimeBetweenEvictionRunsMillis(pool.getTimeBetweenEvictionRuns().toMillis());
}
if (pool.getMaxWait() != null) {
config.setMaxWaitMillis(pool.getMaxWait().toMillis());
}
return config;
} private void customizeConfigurationFromUrl(JedisClientConfiguration.JedisClientConfigurationBuilder builder) {
ConnectionInfo connectionInfo = parseUrl(this.properties.getUrl());
if (connectionInfo.isUseSsl()) {
builder.useSsl();
}
} private void customize(JedisClientConfiguration.JedisClientConfigurationBuilder builder) {
this.builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
} }

  redis配置类:

@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(); public int getDatabase() {
return this.database;
} public void setDatabase(int database) {
this.database = database;
} public String getUrl() {
return this.url;
} public void setUrl(String url) {
this.url = url;
} public String getHost() {
return this.host;
} public void setHost(String host) {
this.host = host;
} public String getPassword() {
return this.password;
} public void setPassword(String password) {
this.password = password;
} public int getPort() {
return this.port;
} public void setPort(int port) {
this.port = port;
} public boolean isSsl() {
return this.ssl;
} public void setSsl(boolean ssl) {
this.ssl = ssl;
} public void setTimeout(Duration timeout) {
this.timeout = timeout;
} public Duration getTimeout() {
return this.timeout;
} public Sentinel getSentinel() {
return this.sentinel;
} public void setSentinel(Sentinel sentinel) {
this.sentinel = sentinel;
} public Cluster getCluster() {
return this.cluster;
} public void setCluster(Cluster cluster) {
this.cluster = cluster;
} public Jedis getJedis() {
return this.jedis;
} public Lettuce getLettuce() {
return this.lettuce;
} /**
* Pool properties.
*/
public static class Pool { /**
* Maximum number of "idle" connections in the pool. Use a negative value to
* indicate an unlimited number of idle connections.
*/
private int maxIdle = 8; /**
* Target for the minimum number of idle connections to maintain in the pool. This
* setting only has an effect if both it and time between eviction runs are
* positive.
*/
private int minIdle = 0; /**
* Maximum number of connections that can be allocated by the pool at a given
* time. Use a negative value for no limit.
*/
private int maxActive = 8; /**
* Maximum amount of time a connection allocation should block before throwing an
* exception when the pool is exhausted. Use a negative value to block
* indefinitely.
*/
private Duration maxWait = Duration.ofMillis(-1); /**
* Time between runs of the idle object evictor thread. When positive, the idle
* object evictor thread starts, otherwise no idle object eviction is performed.
*/
private Duration timeBetweenEvictionRuns; public int getMaxIdle() {
return this.maxIdle;
} public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
} public int getMinIdle() {
return this.minIdle;
} public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
} public int getMaxActive() {
return this.maxActive;
} public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
} public Duration getMaxWait() {
return this.maxWait;
} public void setMaxWait(Duration maxWait) {
this.maxWait = maxWait;
} public Duration getTimeBetweenEvictionRuns() {
return this.timeBetweenEvictionRuns;
} public void setTimeBetweenEvictionRuns(Duration timeBetweenEvictionRuns) {
this.timeBetweenEvictionRuns = timeBetweenEvictionRuns;
} } /**
* Cluster properties.
*/
public static class Cluster { /**
* Comma-separated list of "host:port" pairs to bootstrap from. This represents an
* "initial" list of cluster nodes and is required to have at least one entry.
*/
private List<String> nodes; /**
* Maximum number of redirects to follow when executing commands across the
* cluster.
*/
private Integer maxRedirects; public List<String> getNodes() {
return this.nodes;
} public void setNodes(List<String> nodes) {
this.nodes = nodes;
} public Integer getMaxRedirects() {
return this.maxRedirects;
} public void setMaxRedirects(Integer maxRedirects) {
this.maxRedirects = maxRedirects;
} } /**
* Redis sentinel properties.
*/
public static class Sentinel { /**
* Name of the Redis server.
*/
private String master; /**
* Comma-separated list of "host:port" pairs.
*/
private List<String> nodes; public String getMaster() {
return this.master;
} public void setMaster(String master) {
this.master = master;
} public List<String> getNodes() {
return this.nodes;
} public void setNodes(List<String> nodes) {
this.nodes = nodes;
} } /**
* Jedis client properties.
*/
public static class Jedis { /**
* Jedis pool configuration.
*/
private Pool pool; public Pool getPool() {
return this.pool;
} public void setPool(Pool pool) {
this.pool = pool;
} } /**
* Lettuce client properties.
*/
public static class Lettuce { /**
* Shutdown timeout.
*/
private Duration shutdownTimeout = Duration.ofMillis(100); /**
* Lettuce pool configuration.
*/
private Pool pool; public Duration getShutdownTimeout() {
return this.shutdownTimeout;
} public void setShutdownTimeout(Duration shutdownTimeout) {
this.shutdownTimeout = shutdownTimeout;
} public Pool getPool() {
return this.pool;
} public void setPool(Pool pool) {
this.pool = pool;
} } }

三、Spring Boot自动装配流程图

Spring Boot系列(二):Spring Boot自动装配原理解析

四、总结

  本文以Spring Boot整合Redis为例,把Spring Boot整合第三方组件的自动装配原理进行了解析,对应其他的第三方组件,比如整合Mybatis,套路是一样的。