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>&lt;URL&gt; 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&lt;&gt;<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&lt;?, ?&gt;<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&lt;JedisClientConfigurationBuilderCustomizer&gt;<span style="color: rgba(0, 0, 0, 1)"> builderCustomizers;

JedisConnectionConfiguration(RedisProperties properties,
                             ObjectProvider</span>&lt;RedisSentinelConfiguration&gt;<span style="color: rgba(0, 0, 0, 1)"> sentinelConfiguration,
                             ObjectProvider</span>&lt;RedisClusterConfiguration&gt;<span style="color: rgba(0, 0, 0, 1)"> clusterConfiguration,
                             ObjectProvider</span>&lt;JedisClientConfigurationBuilderCustomizer&gt;<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 启动器。