深刻springboot原理——一步步分析springboot啓動機制(starter機制)

前言

使用過springboot的同窗應該已經知道,springboot經過默認配置了不少框架的使用方式幫咱們大大簡化了項目初始搭建以及開發過程。本文的目的就是一步步分析springboot的啓動過程,分析springboot是如何幫咱們簡化這個過程的。java

springboot幫咱們作了什麼

一般搭建一個基於spring的web應用,咱們須要作如下工做:mysql

一、pom文件中引入相關jar包,包括spring、springmvc、redis、mybaits、log4j、mysql-connector-java 等等相關jar ...web

二、配置web.xml,Listener配置、Filter配置、Servlet配置、log4j配置、error配置 ...redis

三、配置數據庫鏈接、配置spring事務spring

四、配置視圖解析器sql

五、開啓註解、自動掃描功能數據庫

六、配置完成後部署tomcat、啓動調試tomcat

......springboot

搭個初始項目不一會就一個小時甚至半天過去了。而用springboot後,一切都變得很簡便快速。下來咱們來一步步分析springboot的起步依賴與自動配置這兩個核心原理。mybatis

起步依賴

在springboot中咱們只須要引入下面簡單的幾步就能夠完成一個ssm後臺項目的初始搭建。

一、引入jar

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<!--mybatis 開發包-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<!--springboot web模塊支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!--druid 的數據源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.31</version>
</dependency>

spring-boot-starter-web包自動幫咱們引入了web模塊開發須要的相關jar包,

mybatis-spring-boot-starter幫咱們引入了dao開發相關的jar包。

spring-boot-starter-xxx是官方提供的starter,xxx-spring-boot-starter是第三方提供的starter。

以下截圖:

能夠看出在這個mybatis-spring-boot-starter 中,並無任何源碼,只有一個pom文件,它的做用就是幫咱們引入了相關jar包。

二、配置數據源

spring:
  datasource:
     url: jdbc:mysql://127.0.0.1:3306/mybatis_test
     username: root
     password: root
     driver-class-name: com.mysql.jdbc.Driver
     type: com.alibaba.druid.pool.DruidDataSource
     dbcp2:
       min-idle: 5
       initial-size: 5
       max-total: 5
       max-wait-millis: 200

 stater機制幫咱們完成了項目起步所須要的的相關jar包。那問題又來了,傳統的spring應用中不是要在application.xml中配置不少bean的嗎,好比dataSource的配置,transactionManager的配置 ... springboot是如何幫咱們完成這些bean的配置的?下面咱們來分析這個過程

自動配置

基於java代碼的bean配置

以mybatis爲例,在上面的截圖中,咱們發下mybatis-spring-boot-starter這個包幫咱們引入了mybatis-spring-boot-autoconfigure這個包,以下圖:

裏面有MybatisAutoConfiguration這個類,打開這個類看看有什麼東西。

熟悉@Configuration&、@Bean這兩個bean的同窗或許已經知道了。這兩個註解一塊兒使用就能夠建立一個基於java代碼的配置類,能夠用來替代相應的xml配置文件。

@Configuration註解的類能夠看做是能生產讓Spring IoC容器管理的Bean實例的工廠。

@Bean註解告訴Spring,一個帶有@Bean的註解方法將返回一個對象,該對象應該被註冊到spring容器中。

 傳統的基於xml的bean配置方法以下:

<beans>  
    <bean id = "car" class="com.itpsc.Car">  
        <property name="wheel" ref = "wheel"></property>  
    </bean>  
    <bean id = "wheel" class="com.itpsc.Wheel"></bean>  
</beans>

至關於用基於java代碼的配置方式:

@Configuration  
public class Conf {  
    @Bean  
    public Car car() {  
        Car car = new Car();  
        car.setWheel(wheel());  
        return car;  
    }  
    @Bean   
    public Wheel wheel() {  
        return new Wheel();  
    }  
}

因此上面的MybatisAutoConfiguration這個類,自動幫咱們生成了SqlSessionFactory這些Mybatis的重要實例並交給spring容器管理,從而完成bean的自動註冊。

自動配置條件依賴

MybatisAutoConfiguration這個類中使用的註解能夠看出,要完成自動配置是有依賴條件的。

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisAutoConfiguration {
//....
}

這些是springboot特有的,常見的條件依賴註解有:

@ConditionalOnBean,僅在當前上下文中存在某個bean時,纔會實例化這個Bean。

@ConditionalOnClass,某個class位於類路徑上,纔會實例化這個Bean。

@ConditionalOnExpression,當表達式爲true的時候,纔會實例化這個Bean。

@ConditionalOnMissingBean,僅在當前上下文中不存在某個bean時,纔會實例化這個Bean。

@ConditionalOnMissingClass,某個class在類路徑上不存在的時候,纔會實例化這個Bean。

@ConditionalOnNotWebApplication,不是web應用時纔會實例化這個Bean。

@AutoConfigureAfter,在某個bean完成自動配置後實例化這個bean。

@AutoConfigureBefore,在某個bean完成自動配置前實例化這個bean。

因此要完成Mybatis的自動配置,須要在類路徑中存在SqlSessionFactory.class、SqlSessionFactoryBean.class這兩個類,須要存在DataSource這個bean且這個bean完成自動註冊。

進入DataSourceAutoConfiguration這個類,能夠看到這個類屬於這個包:

org.springframework.boot.autoconfigure.jdbc

這個包又屬於spring-boot-autoconfigure-2.0.4.RELEASE.jar這個包,自動配置這個包幫們引入了jdbc、kafka、logging、mail、mongo等包。不少包須要咱們引入相應jar後自動配置才生效。

bean參數獲取

到此咱們已經知道了bean的配置過程,可是尚未看到springboot是如何讀取yml或者properites配置文件的的屬性來建立數據源的?

在DataSourceAutoConfiguration類裏面,咱們注意到使用了EnableConfigurationProperties這個註解。

@Configuration
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
...
}

DataSourceProperties中封裝了數據源的各個屬性,且使用了註解ConfigurationProperties指定了配置文件的前綴。

@ConfigurationProperties(
    prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    private ClassLoader classLoader;
    private String name;
    private boolean generateUniqueName;
    private Class<? extends DataSource> type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String jndiName;
    ...
}

@EnableConfigurationProperties@ConfigurationProperties這兩個註解有什麼用呢?咱們先看一個例子:

@Component
@ConfigurationProperties(prefix="spring.datasource")
public class PropertiesBean {
    private String url;
    private String username;
    private String password;
    //省略getter、setter...
    @Override
    public String toString() {
        return "PropertiesBean{" +
                "url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
@SpringBootApplication
@MapperScan("com.itpsc.mapper*")
@EnableConfigurationProperties
public class SpringbootMybatisDemoApplication {
    public static void main(String[] args) {
        //SpringApplication.run(SpringbootMybatisDemoApplication.class, args);
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootMybatisDemoApplication.class, args);
         //獲取yml配置轉換後的bean
        System.out.println("----------------------"+context.getBean(PropertiesBean.class));
        context.close();
    }
}

運行結果:

 

從運行結果能夠看出@ConfigurationProperties@EnableConfigurationPropertie的做用就是:

@ConfigurationProperties註解的做用是把yml或者properties配置文件轉化爲bean

@EnableConfigurationProperties註解的做用是使@ConfigurationProperties註解生效。若是隻配置@ConfigurationProperties註解,在spring容器中是獲取不到yml或者properties配置文件轉化的bean的。

 

經過這種方式,把yml或者properties配置參數轉化爲bean,這些bean又是如何被發現與加載的?

bean發現

springboot默認掃描啓動類所在的包下的主類與子類的全部組件,但並無包括依賴包的中的類,那麼依賴包中的bean是如何被發現和加載的?

 

咱們一般在啓動類中加@SpringBootApplication這個註解,點進去看

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}

實際上重要的只有三個Annotation

@Configuration@SpringBootConfiguration裏面仍是應用了@Configuration

@EnableAutoConfiguration

@ComponentScan

 

@Configuration的做用上面咱們已經知道了,被註解的類將成爲一個bean配置類。

@ComponentScan的做用就是自動掃描並加載符合條件的組件,好比@Component@Repository等,最終將這些bean定義加載到spring容器中。

@EnableAutoConfiguration 這個註解的功能很重要,藉助@Import的支持,收集和註冊依賴包中相關的bean定義。

 

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

如上源碼,@EnableAutoConfiguration註解引入了@AutoConfigurationPackage@Import這兩個註解。@AutoConfigurationPackage的做用就是自動配置的包,@Import導入須要自動配置的組件。

 

進入@AutoConfigurationPackage,發現也是引入了@Import註解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, new String[]{(new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()});
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
        }
    }

new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()

new AutoConfigurationPackages.PackageImport(metadata)

這兩句代碼的做用就是加載啓動類所在的包下的主類與子類的全部組件註冊到spring容器,這就是前文所說的springboot默認掃描啓動類所在的包下的主類與子類的全部組件。

 

那問題又來了,要蒐集並註冊到spring容器的那些beans來自哪裏?

進入 AutoConfigurationImportSelector類,

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final String[] NO_IMPORTS = new String[0];
...
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if(!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return StringUtils.toStringArray(configurations);
        }
    }
...

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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方法調用loadSpringFactories方法從全部的jar包中讀取META-INF/spring.factories文件信息

 

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap result = (MultiValueMap)cache.get(classLoader);
        if(result != null) {
            return result;
        } else {
            try {
                Enumeration ex = classLoader != null?classLoader.getResources("META-INF/spring.factories"):ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result1 = new LinkedMultiValueMap();

                while(ex.hasMoreElements()) {
                    URL url = (URL)ex.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry entry = (Entry)var6.next();
                        List factoryClassNames = Arrays.asList(StringUtils.commaDelimitedListToStringArray((String)entry.getValue()));
                        result1.addAll((String)entry.getKey(), factoryClassNames);
                    }
                }

                cache.put(classLoader, result1);
                return result1;
            } catch (IOException var9) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var9);
            }
        }
    }

 

下面是spring-boot-autoconfigure這個jarspring.factories文件部份內容,其中有一個keyorg.springframework.boot.autoconfigure.EnableAutoConfiguration的值定義了須要自動配置的bean,經過讀取這個配置獲取一組@Configuration類。

 

org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\

 

每一個xxxAutoConfiguration都是一個基於javabean配置類。實際上,這些xxxAutoConfiguratio不是全部都會被加載,會根據xxxAutoConfiguration上的@ConditionalOnClass等條件判斷是否加載。

 

private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
        try {
            Class ex = ClassUtils.forName(instanceClassName, classLoader);
            if(!factoryClass.isAssignableFrom(ex)) {
                throw new IllegalArgumentException("Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
            } else {
                return ReflectionUtils.accessibleConstructor(ex, new Class[0]).newInstance(new Object[0]);
            }
        } catch (Throwable var4) {
            throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), var4);
        }
    }

 如上代碼段,經過反射機制將spring.factories@Configuration類實例化爲對應的java實列。到此咱們已經知道怎麼發現要自動配置的bean了,最後一步就是怎麼樣將這些bean加載到spring容器。

bean加載

若是要讓一個普通類交給Spring容器管理,一般有如下方法:

1、使用 @Configuration@Bean 註解

2、使用@Controller @Service @Repository @Component 註解標註該類,而後啓用@ComponentScan自動掃描

3、使用@Import 方法

 

springboot中使用了@Import 方法

@EnableAutoConfiguration註解中使用了@Import({AutoConfigurationImportSelector.class})註解,AutoConfigurationImportSelector實現了DeferredImportSelector接口,

DeferredImportSelector接口繼承了ImportSelector接口,ImportSelector接口只有一個selectImports方法。

 

public class AutoConfigurationImportSelector implements DeferredImportSelector{
...
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if(!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return StringUtils.toStringArray(configurations);
        }
}
...
}
public interface DeferredImportSelector extends ImportSelector {
    @Nullable
    default Class<? extends DeferredImportSelector.Group> getImportGroup() {
        return null;
}
public interface Group {...}
}
public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}

 咱們先經過一個簡單例子看看@Import註解是如何將bean導入到spring容器的。

1、新建一個bean

 

public class User {
    private Long id;
    private String name;
    private String password;
private String phone;
...
}

2、建立一個ItpscSelector類繼承ImportSelector接口並實現selectImports方法

public class ItpscSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.itpsc.entity.User"};
    }
}

三、建立ImportConfig類,使用@Configuration@Import(ItpscSelector.class)註解。

@Configuration
@Import(ItpscSelector.class)
public class ImportConfig {
}

四、從容器獲取bean

@RunWith(SpringRunner.class)
@SpringBootTest
public class ImportSelectorTests {
    @Test
    public void testSelectImport() {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(ImportConfig.class);
        String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
        for (String name : beanDefinitionNames) {
            System.out.println(name);
        }
    }
}

運行結果:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
importConfig
com.itpsc.entity.User

很直觀,selectImports方法返回一組bean@EnableAutoConfiguration註解藉助@Import註解將這組bean注入到spring容器中,springboot正式經過這種機制來完成bean的注入的。

總結

咱們能夠將自動配置的關鍵幾步以及相應的註解總結以下:

1@Configuration&@Bean->基於java代碼的bean配置

2@Conditional->設置自動配置條件依賴

3@EnableConfigurationProperties@ConfigurationProperties->讀取配置文件轉換爲bean

4@EnableAutoConfiguration@AutoConfigurationPackage @Import->實現bean發現與加載。

相關文章
相關標籤/搜索