Spring Boot 2.2.0でLazy Initializationを試してみた

## Spring Boot v2.2.0

Spring Boot v2.2.0が[リリース](https://spring.io/blog/2019/10/16/spring-boot-2-2-0)され、GAとなりました。

なので、その機能の一つであるLazyInitializationについて検証してみます。

[Spring Boot 2.2.0](https://spring.io/blog/2019/10/16/spring-boot-2-2-0)
[Release v2.2.0.RELEASE · spring-projects/spring-boot · GitHub](https://github.com/spring-projects/spring-boot/releases/tag/v2.2.0.RELEASE)
[Spring Boot 2.2 Release Notes · spring-projects/spring-boot Wiki · GitHub](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.2-Release-Notes)
[Spring Boot Reference Documentation](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/html/)

## 環境

- Windows10
- OracleJDK 13(build 13)
- Spring Boot 2.2.0 RELEAS

## Projectの作成

ひながたとなるProjectは、Spring Initializrから作成します。

![spring-initializr.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488212/c2d4c8af-56ea-cad3-735a-6078bb87a866.png)

## Gradle
Spring Initializrで作成したGradleプロジェクトでは、Gradle 5.6.2が適用されることを確認しました。

```
> gradlew -version

------------------------------------------------------------
Gradle 5.6.2
------------------------------------------------------------

Build time:   2019-09-05 16:13:54 UTC
Revision:     55a5e53d855db8fc7b0e494412fc624051a8e781

Kotlin:       1.3.41
Groovy:       2.5.4
Ant:          Apache Ant(TM) version 1.9.14 compiled on March 12 2019
JVM:          13-ea (Oracle Corporation 13-ea+33)
OS:           Windows 10 10.0 amd64
```


## [Lazy Initialization](https://spring.io/blog/2019/10/16/spring-boot-2-2-0#lazy-initialization)

ApplicationContextで管理するBeanの生成を、アプリケーションの起動時(≒ApplicationContextの初期化時)ではなくて、対象のBeanの呼び出しがされる際に行うのが、Lazy Initializationです。

この機構は以前のバージョンからもありますが、v2.2.0からはproperty`spring.main.lazy-initialization`にtrueを適用することで、一律LazyInitializationが適用されます。

- application.yml

```yml
spring:
  main:
    lazy-initialization: true
```

- Bean class

```java
package jp.co.musako.domain.model;

public class LazyInitializationDemoBean {

    private String message;

    public LazyInitializationDemoBean(String message) {
        this.message = message;
        System.out.println("call constructor " + this.message);
    }

    public void writer(String message){
        System.out.println(this.message + " is " + message);
    }
}
```

- config class

```java
package jp.co.musako.config;

import jp.co.musako.domain.model.LazyInitializationDemoBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {

    @Bean("lazyInitializationDemoBean1")
    public LazyInitializationDemoBean lazyInitializationDemoBean1(){
        return new LazyInitializationDemoBean("create lazyInitializationDemoBean1");
    }

    @Bean("lazyInitializationDemoBean2")
    public LazyInitializationDemoBean lazyInitializationDemoBean2(){
        return new LazyInitializationDemoBean("create lazyInitializationDemoBean2");
    }
}
```

- main class

```java
package jp.co.musako;

import jp.co.musako.domain.model.LazyInitializationDemoBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
        System.out.println("Initialize Application Context");

        LazyInitializationDemoBean bean1 = ctx.getBean("lazyInitializationDemoBean1", LazyInitializationDemoBean.class);
        bean1.writer("first bean");

        LazyInitializationDemoBean bean2 = ctx.getBean("lazyInitializationDemoBean2", LazyInitializationDemoBean.class);
        bean2.writer("second bean");
    }
}

```

実行結果

```
>gradlew bootRun

> Task :bootRun

2019-10-23 18:16:37.620  INFO 12708 --- [           main] jp.co.musako.Application                 : Started Application in 4.295 seconds (JVM running for 4.933)
Initialize Application Context
call constructor lazyInitializationDemoBean1
lazyInitializationDemoBean1 is first bean
call constructor lazyInitializationDemoBean2
lazyInitializationDemoBean2 is second bean
```
ここで、以下の順序で処理がされていることがわかります。

1. アプリケーション起動
   - 起動後に「Initialize Application Context」を出力
2. Beanの生成
   - Beanのコンストラクタ内で「call constructor lazyInitializationDemoBean1」を出力
3. 生成したBeanのwriterメソッドが呼び出される
   - writerメソッドで「lazyInitializationDemoBean1 is first bean」を出力

## Lazy Initializationの適用除外

`spring.main.lazy-initialization=true`を設定してLazyInitializationをプロジェクトに適用しつつ、特定のBeanをLazyInitializationの対象外とするには以下の方法があります。

1. `@org.springframework.context.annotation.Lazy(false)`をBeanに設定する
    - [Lazy (Spring Framework 5.2.0.RELEASE API)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Lazy.html)
3. `org.springframework.boot.LazyInitializationExcludeFilter`に対象外とするBeanを登録する
   - [LazyInitializationExcludeFilter (Spring Boot Docs 2.2.0.BUILD-SNAPSHOT API)](https://docs.spring.io/spring-boot/docs/2.2.0.BUILD-SNAPSHOT/api//org/springframework/boot/LazyInitializationExcludeFilter.html)


- config class

```java
package jp.co.musako.config;

import jp.co.musako.domain.model.ExcludeLazyInitializationDemoBean;
import jp.co.musako.domain.model.LazyInitializationDemoBean;
import org.springframework.boot.LazyInitializationExcludeFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Configuration
public class Config {
    
    @Bean
    static LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
        return LazyInitializationExcludeFilter.forBeanTypes(ExcludeLazyInitializationDemoBean.class);
    }

    // Lazy Initialization適用対象
    @Bean("lazyInitializationDemoBean1")
    public LazyInitializationDemoBean lazyInitializationDemoBean1(){
        return new LazyInitializationDemoBean("lazyInitializationDemoBean1");
    }

    // Lazy Initialization適用対象外
    @Bean("lazyInitializationDemoBean2")
    @Lazy(false)
    public LazyInitializationDemoBean lazyInitializationDemoBean2(){
        return new LazyInitializationDemoBean("lazyInitializationDemoBean2");
    }

    // Lazy Initialization適用対象外
    @Bean
    public ExcludeLazyInitializationDemoBean ExcludeLazyInitializationDemoBean(){
        return new ExcludeLazyInitializationDemoBean("excludeLazyInitializationDemoBean");
    }
}
```

- main class

```java
package jp.co.musako;

import jp.co.musako.domain.model.ExcludeLazyInitializationDemoBean;
import jp.co.musako.domain.model.LazyInitializationDemoBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
        System.out.println("Initialize Application Context");

        LazyInitializationDemoBean bean1 = ctx.getBean("lazyInitializationDemoBean1", LazyInitializationDemoBean.class);
        bean1.writer("first bean");

        LazyInitializationDemoBean bean2 = ctx.getBean("lazyInitializationDemoBean2", LazyInitializationDemoBean.class);
        bean2.writer("second bean");

        ExcludeLazyInitializationDemoBean ExcludeLazyInitializationBean = ctx.getBean("ExcludeLazyInitializationDemoBean", ExcludeLazyInitializationDemoBean.class);
        ExcludeLazyInitializationBean.writer("third bean");
    }

}

```


- 実行結果

```
>gradlew bootRun

> Task :bootRun

call constructor lazyInitializationDemoBean2
call constructor excludeLazyInitializationDemoBean
2019-10-23 18:52:52.464  INFO 6004 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-10-23 18:52:52.468  INFO 6004 --- [           main] jp.co.musako.Application                 : Started Application in 2.978 seconds (JVM running for 3.454)
Initialize Application Context
call constructor lazyInitializationDemoBean1
lazyInitializationDemoBean1 is first bean
lazyInitializationDemoBean2 is second bean
excludeLazyInitializationDemoBean is third bean
```
上記のように、Config.javaで定義した3つのBeanのうち、lazyInitializationDemoBean1のみがLazyInitializationの対象となることがわかります。

ソースコードはこちら：[GitHub - forests-k/spring-boot-lazy-initialization
: Spring Boot v2.2.0 lazy-initialization sample](https://github.com/forests-k/spring-boot-lazy-initialization)

## Lazy Initializationの適用時の検討事項

Lazy Intializationを導入するメリットは、アプリケーション起動時の速度が向上すること、必要な時に初期化するため無駄にメモリを消費しないことにあります。

しかし、必要な時にBeanを初期化してコンテナにBeanを保持するということは、これまでのBeanのライフサイクルが一部変更となり、コンテナ内で保持するBeanの最大となりうる数量（＝メモリ占有量）がみえないことがあるので、CPUへの負荷やロードアベレージへの影響がある、すなわちLazyInitializationを過信しすぎてはいけない、という可能性があるかもしれないです。

そのため、テスト環境や本番同等の環境で一時的にLazy Initializationを無効化するなどのテストを実施することも視野に入れるべきと思われます。

## ref
- [Lazy Initialization in Spring Boot 2.2](https://spring.io/blog/2019/03/14/lazy-initialization-in-spring-boot-2-2)
- [Spring Boot Reference Documentation](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/html/)
- [appendix Common Application properties](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/html/appendix-application-properties.html)