Spring Boot系列(二):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; }</span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * The location to look for factories. Can be present in multiple JAR files. * FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> Enumeration</span><URL> urls = (classLoader != <span style="color: rgba(0, 0, 255, 1)">null</span> ?<span style="color: rgba(0, 0, 0, 1)"> classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> LinkedMultiValueMap<><span style="color: rgba(0, 0, 0, 1)">(); </span><span style="color: rgba(0, 0, 255, 1)">while</span><span style="color: rgba(0, 0, 0, 1)"> (urls.hasMoreElements()) { URL url </span>=<span style="color: rgba(0, 0, 0, 1)"> urls.nextElement(); UrlResource resource </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> UrlResource(url); Properties properties </span>=<span style="color: rgba(0, 0, 0, 1)"> PropertiesLoaderUtils.loadProperties(resource); </span><span style="color: rgba(0, 0, 255, 1)">for</span> (Map.Entry<?, ?><span style="color: rgba(0, 0, 0, 1)"> entry : properties.entrySet()) { String factoryClassName </span>=<span style="color: rgba(0, 0, 0, 1)"> ((String) entry.getKey()).trim(); </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result; } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException ex) { </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> IllegalArgumentException("Unable to load factories from location [" +<span style="color: rgba(0, 0, 0, 1)"> FACTORIES_RESOURCE_LOCATION </span>+ "]"<span style="color: rgba(0, 0, 0, 1)">, ex); } }</span></pre>
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 {</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * redis配置 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">final</span><span style="color: rgba(0, 0, 0, 1)"> RedisProperties properties; </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">final</span> ObjectProvider<JedisClientConfigurationBuilderCustomizer><span style="color: rgba(0, 0, 0, 1)"> builderCustomizers; JedisConnectionConfiguration(RedisProperties properties, ObjectProvider</span><RedisSentinelConfiguration><span style="color: rgba(0, 0, 0, 1)"> sentinelConfiguration, ObjectProvider</span><RedisClusterConfiguration><span style="color: rgba(0, 0, 0, 1)"> clusterConfiguration, ObjectProvider</span><JedisClientConfigurationBuilderCustomizer><span style="color: rgba(0, 0, 0, 1)"> builderCustomizers) { </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">(properties, sentinelConfiguration, clusterConfiguration); </span><span style="color: rgba(0, 0, 255, 1)">this</span>.properties =<span style="color: rgba(0, 0, 0, 1)"> properties; </span><span style="color: rgba(0, 0, 255, 1)">this</span>.builderCustomizers =<span style="color: rgba(0, 0, 0, 1)"> builderCustomizers; } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Jedis连接工厂 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span><span style="color: rgba(0, 128, 0, 1)"> * </span><span style="color: rgba(128, 128, 128, 1)">@throws</span><span style="color: rgba(0, 128, 0, 1)"> UnknownHostException </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Bean @ConditionalOnMissingBean(RedisConnectionFactory.</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span> JedisConnectionFactory redisConnectionFactory() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> UnknownHostException { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> createJedisConnectionFactory(); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Jedis连接工厂 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> JedisConnectionFactory createJedisConnectionFactory() { JedisClientConfiguration clientConfiguration </span>=<span style="color: rgba(0, 0, 0, 1)"> getJedisClientConfiguration(); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (getSentinelConfig() != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> JedisConnectionFactory(getSentinelConfig(), clientConfiguration); } </span><span style="color: rgba(0, 0, 255, 1)">if</span> (getClusterConfiguration() != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> JedisConnectionFactory(getClusterConfiguration(), clientConfiguration); } </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> JedisConnectionFactory(getStandaloneConfig(), clientConfiguration); } ......</span><span style="color: rgba(0, 0, 255, 1)"><br></span></pre>
redis 对应的属性配置类:
@ConfigurationProperties(prefix = "spring.redis") public class RedisProperties {</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Database index used by the connection factory. </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">int</span> database = 0<span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Connection URL. Overrides host, port, and password. User is ignored. Example: * redis://user:password@example.com:6379 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String url; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Redis server host. </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> String host = "localhost"<span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Login password of the redis server. </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String password; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Redis server port. </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">int</span> port = 6379<span style="color: rgba(0, 0, 0, 1)">; ......</span><span style="color: rgba(0, 0, 0, 1)">
}
三、Spring Boot 自动装配流程图
四、自定义 starter 启动器
1、创建一个工程 toby-spring-boot-autoconfigure,用来编写启动器的核心逻辑
① 在工程的 pom.xml 中添加依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> </dependency> </dependencies>
② 创建一个业务相关类:
/** * @desc: 具体业务 bean * @author: toby */ public class TobyBean {</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 是否开启 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Boolean enabled; </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> TobyBean(Boolean enabled){ </span><span style="color: rgba(0, 0, 255, 1)">this</span>.enabled =<span style="color: rgba(0, 0, 0, 1)"> enabled; } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 检测是否开启toby功能 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String checkTobyFunction(){ </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(enabled){ </span><span style="color: rgba(0, 0, 255, 1)">return</span> "open toby function"<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">return</span> "not open toby function"<span style="color: rgba(0, 0, 0, 1)">; }
}
③ 创建一个 Toby 的属性类:
/** * @desc: Configuration properties for Toby. * @author: toby */ @ConfigurationProperties(prefix = TobyProperties.TOBY_PREFIX) @Data public class TobyProperties { public static final String TOBY_PREFIX = "toby";</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 是否开启 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span> Boolean enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
}
④ 创建一个 Toby 自动装配类:
/** * @desc: Toby 自动装配 * @author: toby */ @Configuration @EnableConfigurationProperties(TobyProperties.class) public class TobyAutoConfiguration implements InitializingBean {</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">final</span><span style="color: rgba(0, 0, 0, 1)"> TobyProperties properties; </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> TobyAutoConfiguration(TobyProperties properties){ </span><span style="color: rgba(0, 0, 255, 1)">this</span>.properties =<span style="color: rgba(0, 0, 0, 1)"> properties; } @Bean </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> TobyBean tobyBean(){ TobyBean tobyBean </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> TobyBean(properties.getEnabled()); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> tobyBean; } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> afterPropertiesSet() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">TODO 初始化工作</span>
}
}
⑤在 resources/META-INF 下面新建一个 spring.factories:
⑥ spring.factories 内容如下:
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.toby.spring.boot.autoconfigure.TobyAutoConfiguration
2、创建一个工程 toby-spring-boot-starter,里面没有逻辑,就依赖上面的 toby-spring-boot-autoconfigure
3、测试自定义的 starter 启动器
① 在需要用到该 starter 的工程的 pom.xml 中引入该 starter 依赖:
<!--自定义 starter--> <dependency> <groupId>com.toby</groupId> <artifactId>toby-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
② 需要用到该 starter 的工程的配置文件中,根据需要设置开启属性值:
③ 使用该 starter 的业务 bean(TobyBean):
@RestController public class WebController {@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> TobyBean tobyBean; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 测试toby * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @RequestMapping(</span>"/toby"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String toby() { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> tobyBean.checkTobyFunction(); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 测试web * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @RequestMapping(</span>"/web"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String web() { </span><span style="color: rgba(0, 0, 255, 1)">return</span> "this is spring boot web"<span style="color: rgba(0, 0, 0, 1)">; }
}
④ 启动工程,访问 http://localhost:8080/toby,显示如下:
自此自定义 Spring Boot Starter 启动器完成!!!
五、总结
本文以 Spring Boot 整合 Redis 为例,把 Spring Boot 整合第三方组件的自动装配原理进行了解析,对应其他的第三方组件,比如整合 Mybatis,套路是一样的,根据自动装配原理自定义了一个 Spring Boot Starter 启动器。