SpringBoot起動プロセスのソースコード解析

はじめに

SpringBootは技術革新というより、Springフレームワークの基礎の上でシステム設定を簡素化したものであり、主要機能は自動設定とスターターコンポーネントです

一、SpringBootの主要機能

  1. スタンドアロン実行(SpringBootアプリケーションは他のコンテナに依存せずに単独で実行できます)

  2. 内蔵Servletコンテナ(Servletコンテナが内蔵され、他のWebコンテナに依存しません)

  3. スターターコンポーネントによるMaven設定の簡素化(単一のスターターコンポーネントを依存関係に追加するだけで関連するjarパッケージが自動的に依存され、Maven設定が簡素化されます)

  4. 自動設定の提供(ほとんどのアプリケーションシナリオにデフォルト設定を提供し、アプリケーション設定を大幅に削減します)

二、SpringBootの一般的なアノテーション

アノテーション 修飾対象 用途
@ComponentScan クラス 自動スキャン、特定のクラスを修飾すると、Springはそのクラスが属するパス配下の@Componentで修飾されたすべてのクラスを自動的にスキャンします。basePackagesを使用してスキャンするパッケージのルートディレクトリをカスタマイズすることもできます @ComponentScan(basePackages="com.test.lucky")
@Component クラス 現在のクラスがコンポーネントであることを宣言し、Springコンテナは@Componentで修飾されたクラスを自動的にスキャンしてSpringコンテナにBeanとしてロードします @Component(value="beanName")
@Controller クラス 本質的にComponentであり、現在のクラスがコントローラークラスであることを宣言し、ディスパッチャは@Controllerアノテーションで修飾されたクラスのメソッドをスキャンします @Controller(value="testController")
@RequestMapping メソッド リクエストURLマッピングのメソッドを宣言します @RequestMapping(value="/test")
@ResponseBody メソッド/クラス コントローラーのメソッドが返すオブジェクトを適切なコンバーターを使用して指定された形式に変換し、responseオブジェクトのbody領域に書き込みます。通常、JSONデータまたはXMLデータを返すために使用されます @ResponseBody
@RestController クラス @Controller + @ResponseBodyの組み合わせと同等で、このクラスのすべてのメソッドが自動的に@ResponseBodyアノテーションで修飾されます @RestController(value="testController")
@Service クラス 本質的にComponentであり、現在のクラスがServiceレイヤーのBeanであることを宣言します @Service(value="testService")
@Repository クラス 本質的にComponentであり、現在のクラスがDaoレイヤーのBeanであることを宣言します @Repository(value="testDao")
@Configuration クラス 本質的にComponentであり、現在のクラスが設定クラスであることを示し、プロキシオブジェクトが生成され、現在のクラスが@Beanで定義されたソースクラスであることを示します @Configuration
@Bean メソッド タグに相当し、@Beanで修飾されたメソッドが返すオブジェクトはSpringコンテナにロードされます @Bean(name="beanName")
@Import クラス サードパーティパッケージのBeanをインポートするために使用され、インポートされたBeanは自動的にSpringコンテナにロードされます @Import(value=Test.class)
@Value メソッド/プロパティ 定数、設定値、他のBeanを変数に注入します @Value("${properteis.test}")

三、SpringBoot起動プロセスの解析

SpringBootの機能はSpringフレームワークに基づいているため、起動時の主要なステップにはSpringコンテナの初期化と起動プロセスが不可欠です。また、SpringBootは自動設定機能も提供しています。

1、SpringBootApplication.run()メソッド

SpringBootアプリケーションの起動クラスの例は以下の通りです:

1 @SpringBootApplication
2 public class ApplicationBootstrap {
3 
4     public static void main(String[] args){
5         ApplicationContext applicationContext = SpringApplication.run(ApplicationBootstrap.class);
6     }
7 }

SpringBootアプリケーションの起動エントリーポイントです。コードは比較的シンプルで、@SpringBootApplicationアノテーションで修飾されているだけで、mainメソッドは直接SpringApplicationクラスの静的runメソッドを呼び出します。最終的にはSpringApplicationのインスタンスメソッドrunメソッドが呼び出され、その核心ロジックは以下の通りです:

 1 public ConfigurableApplicationContext run(String... args) {
 2         ConfigurableApplicationContext context = null;
 3         /** 1.リフレクションを使用してApplicationContextオブジェクトを作成 */
 4         context = createApplicationContext();
 5         /** 2.ApplicationContextコンテキスト環境を準備 */
 6         prepareContext(context, environment, listeners, applicationArguments, printedBanner);
 7         /** 3.ApplicationContextをリフレッシュ */
 8         refreshContext(context);
 9         /** 4.ApplicationContextリフレッシュ後処理 */
10         afterRefresh(context, applicationArguments);
11         return context;
12     }

SpringBootアプリケーション起動の核心ロジックは実際には2ステップです。1.**IOCコンテナApplicationContextインスタンスの作成;2.IOCコンテナの起動です。

一方、SpringBootの他の主要機能は@SpringBootApplicationアノテーションによって実装されています。このアノテーションこそがSpringBootの核心実装です。

2、@SpringBootApplicationアノテーション

@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

定義からわかるように、@SpringBootApplicationは複合アノテーションであり、内部に@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScanの3つのアノテーションが含まれています。

2.1、@SpringBootConfiguration

@SpringBootConfigurationアノテーションの定義は以下の通りです:

1 @Target(ElementType.TYPE)
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Configuration
5 public @interface SpringBootConfiguration

からわかるように、@SpringBootConfigurationは本質的に@Configurationアノテーションであり、現在のクラスが設定クラスであることを示します。

2.2、@ComponentScan

@ComponentScanアノテーションの定義は以下の通りです:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan 

@ComponentScanアノテーションは自動スキャンを意味し、現在のクラスのパス配下で@Componentアノテーションで修飾されたすべてのBeanをスキャンします。

2.3、@EnableAutoConfiguration

@EnableAutoConfigurationアノテーションの定義は以下の通りです:

1 @Target(ElementType.TYPE)
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Inherited
5 @AutoConfigurationPackage
6 @Import(AutoConfigurationImportSelector.class)
7 public @interface EnableAutoConfiguration

からわかるように、@EnableAutoConfigurationアノテーションも複合アノテーションであり、@AutoConfigurationPackageと@Importアノテーションを含んでいます。次に@AutoConfigurationPackageアノテーションの定義を見てみましょう:

1 @Target(ElementType.TYPE)
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Inherited
5 @Import(AutoConfigurationPackages.Registrar.class)
6 public @interface AutoConfigurationPackage

このアノテーション内部には@Importアノテーションが含まれているため、結論として@EnableAutoConfigurationは実際には2つの@Importアノテーションで構成されており、それぞれAutoConfigurationImportSelectorとAutoConfigurationPackages.Registrarクラスのインスタンスをインポートします。

したがって、これら2つのクラスがSpringBootの自動設定機能の核心実装であると言えます。

@Importアノテーションの詳細な使用方法についてはここでは詳しく説明しませんが、まず簡単に紹介しましょう。@Importアノテーションには3つの使用方法があります:

1.ロードするBeanを明示的にインポートする:@Import(value=TestService.class)

2.ImportSelectorインターフェースを実装するクラスをインポートする:カスタムのMyImportSelectorクラスを作成し、ImportSelectorを実装し、selectImportsメソッドをオーバーライドして、インポートするBeanの配列を返します。例は以下の通りです:

1 public class MyImportSelector implements ImportSelector {
2     @Override
3     public String[] selectImports(AnnotationMetadata importingClassMetadata) {
4         return new String[]{UserAccountService.class.getName(), ProductService.class.getName()};
5     }
6 }

3.ImportBeanDefinitionRegistrarインターフェースを実装するクラスをインポートする:カスタムのMyImportBeanDefinitionRegistrarを作成し、registBeanDefinitionsメソッドをオーバーライドして、手動でロードするBeanを登録します。例は以下の通りです:

1 public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
2 
3     @Override
4     public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
5                                          BeanNameGenerator importBeanNameGenerator) {
6         /** 手動でロードするBeanを登録*/
7         registerBeanDefinitions(importingClassMetadata, registry);
8     }
9 }

話を戻して@EnableAutoConfigurationアノテーションを見ると、それぞれImportSelectorインターフェースの実装クラスであるAutoConfigurationImportSelectorとImportBeanDefinitionRegistrarの実装クラスであるAutoConfigurationPackages.Registrarをインポートしています。次にそれぞれを分析します。

2.4、AutoConfigurationImportSelector

AutoConfigurationImportSelectorはImportSelectorインターフェースを実装しているため、selectImportsメソッドが返すString配列内のbeanNameをロードします。したがって、重要なのはselectImportsメソッドです。ソースコードは以下の通りです:

 1 public String[] selectImports(AnnotationMetadata annotationMetadata) {
 2         if (!isEnabled(annotationMetadata)) {
 3             return NO_IMPORTS;
 4         }
 5         /** メタデータをロード*/
 6         AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
 7                 .loadMetadata(this.beanClassLoader);
 8         AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
 9                 annotationMetadata);
10         return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
11     }

核心ステップは2つです。最初にloadMetadataメソッドを実行します。ソースコードは以下の通りです:

 1 protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
 2 
 3     static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
 4         /** 指定されたパスのメタデータをロード*/
 5         return loadMetadata(classLoader, PATH);
 6     }
 7 
 8     static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
 9         try {
10             Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
11                     : ClassLoader.getSystemResources(path);
12             Properties properties = new Properties();
13             /** spring-autoconfigure-metadata.properties設定ファイルを反復処理し、データを読み込んでAutoConfigurationMetadataオブジェクトにカプセル化*/
14             while (urls.hasMoreElements()) {
15                 properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
16             }
17             return loadMetadata(properties);
18         }
19         catch (IOException ex) {
20             throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
21         }
22     }

ロジックは、指定されたパスからspring-autoconfigure-metadata.properties設定ファイルを見つけ、反復処理して内容を読み取り、Propertiesオブジェクトに追加し、AutoConfigurationMetadataオブジェクトにカプセル化します。

spring-autoconfigure-metadata.properties設定ファイルはspring-boot-autoconfigureパッケージのMETA-INFディレクトリにあります。現時点ではその役割を気にする必要はありません。現在の結論としては、この設定ファイルの内容がすべてロードされることです。

次にgetAutoConfigurationEntryメソッドの実装を見てみましょう。ソースコードは以下の通りです:

 1  protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
 2                                                                AnnotationMetadata annotationMetadata) {
 3         if (!isEnabled(annotationMetadata)) {
 4             return EMPTY_ENTRY;
 5         }
 6         /** ロードされた設定メタデータをAnnotationAttributesオブジェクトにカプセル化*/
 7         AnnotationAttributes attributes = getAttributes(annotationMetadata);
 8         /** 候補となるすべての設定情報コレクションを取得 (IOCコンテナにロードするBean)
 9          *  実際はMETA-INF/spring.factories設定ファイルのBeanをロードします
10          * */
11         List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
12         /** 重複を削除*/
13         configurations = removeDuplicates(configurations);
14         /** 除外するものを削除*/
15         Set<String> exclusions = getExclusions(annotationMetadata, attributes);
16         checkExcludedClasses(configurations, exclusions);
17         configurations.removeAll(exclusions);
18         /** フィルタリング*/
19         configurations = filter(configurations, autoConfigurationMetadata);
20         fireAutoConfigurationImportEvents(configurations, exclusions);
21         /** AutoConfigurationEntryオブジェクトにカプセル化*/
22         return new AutoConfigurationEntry(configurations, exclusions);
23     }

目的はMETA-INF/spring.factories設定ファイルをロードし、ロードするすべてのクラスを取得し、除外とフィルタリングによって必要なクラスを選別することです。

spring.factoriesにはEnableAutoConfigurationというキーがあり、その値はコレクションであり、ほとんどの一般的なミドルウェアの自動設定クラスが含まれています。これらのクラスはすべてspring-boot-autoconfigureパッケージにあり、一般的なものには以下のようなものがあります:

MongoDataAutoConfiguration、RedisAutoConfiguration、DataSourceAutoConfiguration、RabbitAutoConfiguration、ElasticsearchAutoConfigurationなど、各クラスはそれぞれの自動設定クラスをカプセル化しています。

ヒント:では問題です。SpringBootはデフォルトで様々なコンポーネントの設定クラスを提供していますが、プログラムで関連コンポーネントを使用していない場合、これらの不要な設定クラスもコンテナにロードされるのでしょうか?

答えは明らかに否定です。spring-autoconfigure-metadata.propertiesでは、各コンポーネントのConditionalOnClassのキーが定義されており、値は対応するクラスの完全なパスです。例えば:

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.ConditionalOnClass=org.springframework.data.redis.core.RedisOperations

org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.ConditionalOnClass=com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplate

org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration.ConditionalOnClass=javax.sql.DataSource,org.springframework.jdbc.core.JdbcTemplate

ConditionalOnClassは、対応する属性をロードするために、対応するクラスをロードする必要があることを意味します。例えば、プログラムでRabbitMQを使用している場合、RabbitMQに依存するjarパッケージが確かに含まれているため、RabbitTemplateクラスがロードされます。その場合にのみRabbitAutoConfigurationクラスがロードされ、RedisAutoConfigurationクラスの場合も同様で、前提としてRedisOperationsクラスが先にロードされる必要があります。対応するクラスが先にロードされてのみ、対応するAutoConfigurationクラスがロードされます。

まとめ

  1. SpringBootが起動するときは、@EnableAutoConfigurationアノテーションを通じてAutoConfigurationImportSelectorオブジェクトをインポートし、スキャンパス配下のMETA-INF/spring.factoriesの設定をスキャンします。この設定ファイルには、各コンポーネントの自動設定クラスに対応するAutoConfigurationクラスが含まれており、各コンポーネントのAutoConfigurationクラスをロードすることで対応する自動設定をロードできます。また、ConditionalOnClassを使用して必要な自動設定クラスをフィルタリングし、プログラムで使用していない自動設定クラスを除外します。

  2. @SpringBootApplicationは複合アノテーションであり、それぞれ@SpringBootConfiguration、@ComponentScan、@EnableAutoConfigurationの3つのコンポーネントで構成されています。そのうち@SpringBootConfigurationと@ComponentScanの2つのコンポーネントは、ユーザー定義のBeanをスキャンできることを保証し、@EnableAutoConfigurationはサードパーティのBeanをスキャンしてロードするために使用されます。

  3. SpringBootアプリケーションの起動プロセスは、実際にはIOCコンテナの作成と起動のプロセスであり、ユーザー定義のBeanとサードパーティのBeanをスキャンしてBeanのロードを完了します。

四、SpringBoot組み込みTomcatのソースコード解析

SpringBootがWebコンテナを内蔵しているため、TomcatだけではなくJettyやUndertowも内蔵しています。したがって、設計者の観点からは、これら3つの具体的なコンテナの上に抽象化を行い、WebServerインターフェースを定義しています。WebServerインターフェースの定義は以下の通りです:

public interface WebServer {

    /** Webコンテナを起動*/
    void start() throws WebServerException;

    /** Webコンテナを停止*/
    void stop() throws WebServerException;

    /** Webコンテナがリッスンするポートを取得 */
    int getPort();

}

一方、Tomcat、Jetty、UndertowなどのコンテナはそれぞれWebServerインターフェースを実装する実装クラスを持っており、それぞれTomcatWebServer、JettyWebServer、UndertowWebServerです。そしてSpringBootは工場パターンを使用してどのコンテナを使用するかを決定するため、ServertWebServerFactoryという工場クラスを定義しています。spring.factories設定ファイルにはServletWebServerFactoryAutoConfigurationが定義されており、このクラスの定義は以下の通りです:

 1 @Configuration(proxyBeanMethods = false)
 2 @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
 3 @ConditionalOnClass(ServletRequest.class)
 4 @ConditionalOnWebApplication(type = Type.SERVLET)
 5 @EnableConfigurationProperties(ServerProperties.class)
 6 @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
 7         ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
 8         ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
 9         ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
10 public class ServletWebServerFactoryAutoConfiguration 

このクラスは@Importアノテーションを通じてそれぞれEmbeddedTomcat、EmbeddedJetty、EmbeddedUndertowのBeanをインポートしています。EmbeddedTomcatを例に取ると、定義は以下の通りです:

 1 @Configuration(proxyBeanMethods = false)
 2     @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
 3     @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
 4     public static class EmbeddedTomcat {
 5 
 6         @Bean
 7         public TomcatServletWebServerFactory tomcatServletWebServerFactory(
 8                 ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
 9                 ObjectProvider<TomcatContextCustomizer> contextCustomizers,
10                 ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
11             TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
12             factory.getTomcatConnectorCustomizers()
13                     .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
14             factory.getTomcatContextCustomizers()
15                     .addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
16             factory.getTomcatProtocolHandlerCustomizers()
17                     .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
18             return factory;
19         }
20 
21     }

このクラス内部では@Beanアノテーションを通じてTomcatServletWebServerFactoryオブジェクトを定義しており、これはTomcatWebServerの工場クラスです。TomcatServletWebServerFactoryにはgetWebServerメソッドがあり、定義は以下の通りです:

 1 @Override
 2     public WebServer getWebServer(ServletContextInitializer... initializers) {
 3         if (this.disableMBeanRegistry) {
 4             Registry.disableRegistry();
 5         }
 6         Tomcat tomcat = new Tomcat();
 7         File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
 8         tomcat.setBaseDir(baseDir.getAbsolutePath());
 9         Connector connector = new Connector(this.protocol);
10         connector.setThrowOnFailure(true);
11         tomcat.getService().addConnector(connector);
12         customizeConnector(connector);
13         tomcat.setConnector(connector);
14         tomcat.getHost().setAutoDeploy(false);
15         configureEngine(tomcat.getEngine());
16         for (Connector additionalConnector : this.additionalTomcatConnectors) {
17             tomcat.getService().addConnector(additionalConnector);
18         }
19         prepareContext(tomcat.getHost(), initializers);
20         return getTomcatWebServer(tomcat);
21     }
1 protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
2         return new TomcatWebServer(tomcat, getPort() >= 0);
3     }

ソースコードは複雑ではなく、Tomcatオブジェクトを作成し、パラメータを初期化してからTomcatWebServerのコンストラクタに渡し、TomcatWebServerオブジェクトを作成します。TomcatWebServerのコンストラクタでは、Tomcatのstartメソッドを呼び出してTomcatサーバーを起動します。

ヒント:では問題です。getWebServerメソッドはいつ呼び出されるのでしょうか?

Springコンテナの作成後、リフレッシュ操作が実行されます。ApplicationContextのサブクラスであるServletWebServerApplicationContextは、ApplicationContextのonRefreshメソッドを実装しています。ソースコードは以下の通りです:

1 protected void onRefresh() {
2         super.onRefresh();
3         try {
4             createWebServer();
5         }
6         catch (Throwable ex) {
7             throw new ApplicationContextException("Unable to start web server", ex);
8         }
9     }

まず親クラスのonRefreshメソッドを呼び出し、次にcreateWebServerメソッドを実行してWebServerオブジェクトを作成します。ロジックは、IOCコンテナからServletWebServerFactoryオブジェクトを見つけ、getWebServerメソッドを実行してWebServerオブジェクトを取得します。

apacheのTomcatの実装原理の解析は以下を参照してください:

五、カスタムstarterの実装方法

1.新しいプロジェクトspring-boot-mytest-starterを作成します(公式が提供するstarterはspring-boot-starter-xxxと命名され、カスタムのものは通常spring-boot-xxx-starterと命名され、公式提供のものと区別するために使用されます)

タグ: SpringBoot Java 自動設定 Tomcat IOCコンテナ

6月30日 23:33 投稿