深入springboot原理——一步步分析springboot啟動機制(starter機制)
前言
使用過springboot的同學應該已經知道,springboot通過預設配置了很多框架的使用方式幫我們大大簡化了專案初始搭建以及開發過程。本文的目的就是一步步分析springboot的啟動過程,分析springboot是如何幫我們簡化這個過程的。
springboot幫我們做了什麼
通常搭建一個基於spring的web應用,我們需要做以下工作:
1、pom檔案中引入相關jar包,包括spring、springmvc、redis、mybaits、log4j、mysql-connector-java 等等相關jar ...
2、配置web.xml,Listener配置、Filter配置、Servlet配置、log4j配置、error配置 ...
3、配置資料庫連線、配置spring事務
4、配置檢視解析器
5、開啟註解、自動掃描功能
6、配置完成後部署tomcat、啟動除錯
......
搭個初始專案不一會就一個小時甚至半天過去了。而用springboot後,一切都變得很簡便快速。下來我們來一步步分析springboot的起步依賴與自動配置這兩個核心原理。
起步依賴
在springboot中我們只需要引入下面簡單的幾步就可以完成一個ssm後臺專案的初始搭建。
1、引入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包。
2、配置資料來源
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.test.Car"> <property name="wheel" ref = "wheel"></property> </bean> <bean id = "wheel" class="com.test.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 這個 jar 中 spring.factories 檔案部分內容,其中有一個 key 為 org.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 都是一個基於 java 的 bean 配置類。實際上,這些 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"}; } }
3、 建立 ImportConfig 類,使用 @Configuration 、 @Import(ItpscSelector.class) 註解。
@Configuration @Import(ItpscSelector.class) public class ImportConfig { }
4、 從容器獲取 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 發現與載入。