Spring Boot系列(二):Spring Boot自动装配原理解析
- 2020 年 8 月 18 日
- 筆記
- Spring Boot
一、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注解
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如下:
3、RedisAutoConfiguration分析
导入了三个组件:RedisTemplate,StringRedisTemplate,JedisConnectionConfiguration
① 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:[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(); 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整合Redis为例,把Spring Boot整合第三方组件的自动装配原理进行了解析,对应其他的第三方组件,比如整合Mybatis,套路是一样的。