Skip to content

自动配置原理分析

SpringBoot就是对Spring的一种高度封装,我们也需要深入的学习其原理,方便后续的使用和拓展


Condition

Condition是在Spring4.0增加的条件判断功能,通过这个功能可以实现选择性的创建Bean操作(满足条件则创建,不满足则不创建)

案例:在SpringIOC容器中有一个UserBean,导入Jedis坐标后,就加载该Bean,如果没有导入,则不加载

  • domain中创建一个实体类:

    java
    package com.jlc.springbootcondition.domain;
    
    public class User {}
  • 再编写一个配置类:用于创建User相关的Bean

    java
    package com.jlc.springbootcondition.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import package com.jlc.springbootcondition.domain.User;
    import org.springframework.context.annotation.Conditional;
    import com.jlc.springbootcondition.condition.ClassCondition;
    
    @Configuration
    public class UserConfig {
        @Bean
        @Conditional(ClassCondition.class)
        public User user() {
            return new User();
        }
    }

    @Conditional是核心的条件注解(需要添加条件注解的实现类),其接口Condition中有一个方法matches,其返回值是一个布尔类型的值,如果返回的是True,那么被这个注解修饰的对象将会被Spring容器所创建,反之,不会创建

    编写一个条件注解的实现类:

    java
    package com.jlc.springbootcondition.condition;
    
    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.type.AnnotatedTypeMetadata;
    
    public class ClassCondition implements Condition {
        /**
        	@param context 上下文对象,用于获取环境,IOC容器,ClassLoader
        	@param metadata 注解元对象,可以用于获取注解定义的属性值
        */
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            // 导入Jedis坐标后,就加载该Bean,如果没有导入,则不加载
            // 导入Jedis坐标后,redis.clients.jedis.Jedis文件就存在
            boolean flag = true;
            try {
                Class<?> cls = Class.forName("redis.clients.jedis.Jedis");
            } catch (ClassNotFoundException e) {
                flag = false;
            }
            return false;
        }
    }

    pom.xml配置文件导入jedis坐标:

    xml
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
  • 获取User相关的Bean

    java
    package com.jlc.springbootcondition;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    public class SpringbootConditionApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取user相关的Bean
            Object user = context.getBean("user");
            System.out.println(user);
        }
    }

我们可以对上述的功能进行优化,将类的判断定义为动态的,判断哪个字节码文件存在可动态的指定

我们需要定义一个注解Annotation

java
package com.jlc.springbootcondition.condition;

import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented      // 引入Conditional中的三个源注解
@Conditional(ClassCondition.class)
public @interface ConditionOnClass {
    String[] value();
}

创建User相关的Bean,同时使用我们自己定义的条件注解

java
package com.jlc.springbootcondition.config;

import org.springframework.context.annotation.Bean;
import com.jlc.springbootcondition.condition.ConditionOnClass;
import package com.jlc.springbootcondition.domain.User;
import com.jlc.springbootcondition.condition.ClassCondition;

@Configuration
public class UserConfig {
    @Bean
    @ConditionOnClass("redis.clients.jedis.Jedis")  // 可以动态的指定字节码文件
    public User user() {
        return new User();
    }
}

编写一个条件注解的实现类:

java
package com.jlc.springbootcondition.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import com.jlc.springbootcondition.condition.ConditionOnClass;

public class ClassCondition implements Condition {
    /**
    	@param context 上下文对象,用于获取环境,IOC容器,ClassLoader
    	@param metadata 注解元对象,可以用于获取注解定义的属性值
    */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 导入通过注解属性值value指定坐标,如果指定的坐标存在,则创建Bean,反之,不创建
        // 获取注解属性值 value
        Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
        String[] value = (String[]) map.get("value");
        boolean flag = true;
        try {
            for (String className : value) {
                Class<?> cls = Class.forName(className);
            }
        } catch (ClassNotFoundException e) {
            flag = false;
        }
        return false;
    }
}

通过上述的两个案例,我们就可以知道SpringBoot是如何创建哪个具体的Bean的,如SpringBoot是如何知道要创建RedisTemplate

系统提供的常用条件注解

  • ConditionalOnProperty:判断配置文件中是否有对应属性和值,如果有,初始化Bean

    java
    @Bean
    @ConditionalOnProperty(name = "Jlc", havingValue = "jlc")
    public User user() {
        return new User();
    }

    配置文件中有对应的值和键,才会去初始化对应的Bean

    properties
    Jlc=jlc

    配置完后,就可以初始化对应的Bean

  • ConditionalOnClass:判断环境中是否有对应的字节码文件,如果有,初始化Bean

  • ConditionalOnMissingBean:判断环境中没有对应的Bean才初始化Bean


切换内置web服务器

SpringBootWeb环境中默认使用tomcat作为内置服务器,其实SpringBoot提供了4种内置服务器供我们选择(JettyNettyTomcatUndertow),我们可以进行选择切换(我们只需导入不同服务器Web的坐标,就可以实现服务器的动态切换)

使用tomcat作为内置服务器是需要引入相应的坐标的:

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

切换其他内容Web服务器的方式:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!--排除tomcat依赖-->
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>

<!--引入jetty的依赖-->
<dependency>
	<artifactId>spring-boot-starter-jetty</artifactId>
    <groupId>org.springframework.boot</groupId>
</dependency>

@Enable*注解

SpringBoot引导类中@SpringBootApplication注解的本质就是Configuration,意为着通过该注解修饰的类是一个配置类,是可以直接定义Bean

@Enable*注解表示以@Enable开头的一类注解

问题思考:SpringBoot工程是否可以直接获取jar包(第三方)中定义的Bean?(不能直接获取)

SpringBoot工程不能直接获取在其他工程中定义的Bean,原因是@SpringBootApplication注解只能扫描当前引导类所在的包及其子包,对于第三方包的简单构建:

java
package com.jlc.domain;

public class User {}
java
package com.jlc.config;

import com.jlc.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {
    @Bean
    public User user() {
        return new User();
    }
}

在另一个工程中对这个Bean进行使用,首先需要在pom.xml中进行对第三方包的引入:

xml
<dependency>
    <groupId>com.jlc</groupId>
    <artifactId>springboot-enable-other</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

对于不能直接获取在其他工程中定义的Bean问题,这种情况有以下几种解决方式:

  1. 使用@ComponentScan注解进行扫描我们要加载配置类所在的包

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @ComponentScan("com.jlc.config")
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Object user = context.getBean("user");
            System.out.println(user);
        }
    }

    但是这种解决方法是不严谨的,是比较麻烦的,我们以后使用其他第三方的包是不可能使用这种方式的

  2. 使用@Import注解去加载类,这些类都会被Spring创建,并放入到IOC容器中

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @Import(UserConfig.class)
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Object user = context.getBean("user");
            System.out.println(user);
        }
    }

    但是这样的方式也不是特别方便,我们需要记住类的具体名字

  3. 可以对@Import注解进行封装,在提供Bean的第三方中进行封装

    java
    package com.jlc.config;
    
    import org.springframework.context.annotation.Import;
    import java.lang.annotation.*;
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented 
    @Import(UserConfig.class)
    public @interface EnableUser {}

    在第三方包中封装完后,我们在其他项目中通过@EnableUser注解配置,直接使用即可

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @EnableUser
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Object user = context.getBean("user");
            System.out.println(user);
        }
    }

    SpringBoot中提供了很多以Enable开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用@Import注解导入一些配置类,实现Bean的动态加载

@Import注解

@Enable*注解底层依赖于@Import注解导入一些类,使用@Import注解导入的类会被Spring加载到IOC容器中

@Import提供了四种用法(导入方式):

  • 直接导入Bean

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @Import(User.class)
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Object user = context.getBean(User.class);
            System.out.println(user);
            // 获取这个Bean的具体信息
            Map<String, User> map = context.getBeansOfType(User.class);
            System.out.println(map);
        }
    }
  • 导入配置类(会将配置类中定义的所有Bean导入到IOC容器中)

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @Import(UserConfig.class)
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Object user = context.getBean(User.class);
            System.out.println(user);
        }
    }
  • 导入importSelector接口的实现类,一般用于加载配置文件中的类

    在第三方包中编写importSelector接口的实现类,供后续导入使用:

    java
    package com.jlc.config;
    
    import org.springframework.context.annotation.ImportSelector;
    import org.springframework.core.type.AnnotationMetadata;
    
    public class MyImportSelector implements importSelector {
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            return new String[]{"com.jlc.domain.User"};
        }
    }

    导入importSelector实现类

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @Import(MyImportSelector.class)
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Object user = context.getBean(User.class);
            System.out.println(user);
        }
    }

    这种方式进行导入,可以同时导入多个,而且可以将导入的内容写到配置文件中,进行动态的修改

    系统提供的@SpringBootApplication是使用这种导入方式

  • 导入ImportBeanDefinitionRegistrar接口的实现类

    在第三方包中编写ImportBeanDefinitionRegistrar接口的实现类,供后续导入使用:

    java
    package com.jlc.config;
    
    import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
    import org.springframework.beans.factory.support.AbstractBeanDefinition;
    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.core.type.AnnotationMetadata;
    
    public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
            registry.registerBeanDefinition("user", beanDefinition);
        }
    }

    导入importSelector实现类

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    @Import(MyImportBeanDefinitionRegistrar.class)
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Object user = context.getBean(User.class);
            System.out.println(user);
        }
    }

@EnableAutoConfiguration注解

@EnableAutoConfiguration注解是SpringBoot自动配置的核心注解

@EnableAutoConfiguration注解内部使用@Import(AutoConfigurationImportSelector.class)来加载配置类。配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当SpringBoot应用启动时,会自动加载这些配置类,初始化Bean

不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean


自定义起步依赖

需求:自定义redis-starter,要求当导入redis坐标时,SpringBoot自动创建JedisBean

实现步骤:

  1. 创建redis-sping-boot-autoconfigure模块(Modules模块,用于编写核心的自动配置类)

  2. redis-spring-boot-autoconfigure模块中初始化JedisBean,并定义META-INF/spring.factories文件

    pom.xml中引入jedis依赖:

    xml
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>

    编写一个核心的自动配置类:

    java
    package com.jlc.redis.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.ConditionalOnClass;
    import org.springframework.context.annotation.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import redis.clients.jedis.Jedis;
    
    @Configuration
    @ConditionalOnClass(Jedis.class)   // Jedis.class存在的时候才加载
    @EnableConfigurationProperties(RedisProperties.class)
    public class RedisAutoConfiguration {
        // 提供Jedis的bean
        @Bean
        @ConditionalOnMissingBean(name = "jedis")  // 如果没有以jedis为名字的Bean时,我们才提供
        public Jedis jedis(RedisProperties redisProperties) {
            return new Jedis(redisProperties.getHost(), redisProperties.getPort());
        }
    }

    编写一个实体类,和配置文件进行绑定

    java
    package com.jlc.redis.config;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties(prefix = "redis")
    public class RedisProperties {
        // 如果用户没有配置,默认使用本机的Ip和端口
    	private String host = "localhost";
        private int port = 6379;
        
        public String getHost() { return host; }
        public void setHost(String host) { this.host = host; }
        
        public int getPort() { return port; }
        public void setPort(int port) { this.port = port; }
    }

    在配置文件application.properties中定义配置:

    properties
    redis.host=localhost
    redis.port=6379

    resources文件夹中创建META-INF文件夹,内部新建spring.factories文件,其内容为:

    properties
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.jlc.redis.config.RedisAutoConfiguration
  3. 创建redis-spring-boot-starter模块(进行依赖的整合),依赖redis-spring-boot-autoconfigure模块

    对于该模块的依赖pom.xml,系统创建的依赖只留下核心的依赖即可:

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

    同时引入我们自定义的configure

    xml
    <!--引入自定义的redis的starter-->
    <dependency>
    	<groupId>com.jlc</groupId>
        <artifactId>redis-spring-boot-autoconfigure</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
  4. 在测试模块中引入自定义的redis-starter依赖,测试获取JedisBean,操作redis

    java
    package com.jlc.springbootenable;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    
    @SpringBootApplication
    public class SpringbootEnableApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args);
            
            // 获取Bean
            Jedis jedis = context.getBean(Jedis.class);
            System.out.println(jedis);
            jedis.set("name", "jlc");
            
            String name = jedis.get("name");
            System.out.println(name);
        }
    }

Released under the MIT License.