Spring Boot で main class が複数ある場合のエラー

Spring Boot を実行した際に遭遇したエラー。

今回の検証内容のリポジトリ

./mvnw spring-boot:run
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.4.1:run (default-cli) on project non-web: Execution default-cli of goal org.springframework.boot:spring-boot-maven-plugin:2.4.1:run failed: Unable to find a single main class from the following candidates [com.kiyotakeshi.non.web.Runner3, com.kiyotakeshi.non.web.Runner2, com.kiyotakeshi.non.web.Runner1] -> [Help 1]
[ERROR] 

run failed: Unable to find a single main class とあるように、main class を指定する必要がある。


今回試してみた、 work around は以下の二つ( README.md に記載)。

いずれも、 pom.xmlspring-boot-maven-plugin にて mainClass を指定します。

 <properties>
        <java.version>11</java.version>
        <spring.boot.mainClass>com.kiyotakeshi.non.web.Runner1</spring.boot.mainClass>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>${spring.boot.mainClass}</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

1つ目はビルドした jar のメインクラスの情報を更新してあげるやり方。

まずは、ビルドした jar を解凍し、どのクラスを読み込むかを確認します。

./mvnw clean package

# 任意のディレクトリにて jar を解凍
cp target/non-web-0.0.1-SNAPSHOT.jar /tmp/ 

cd /tmp/ 

unzip non-web-0.0.1-SNAPSHOT.jar

grep -Ei "start|main" META-INF/MANIFEST.MF

mainClass で指定している Runner1 になっています。

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.kiyotakeshi.non.web.Runner1

この .jar を実行する際に、 JVM Options でメインクラスを指定することで、別クラスを実行できます。

java -cp non-web-0.0.1-SNAPSHOT.jar \
-Dloader.main=com.kiyotakeshi.non.web.Runner3 \
org.springframework.boot.loader.PropertiesLauncher

2つ目は Profile 指定により切り替える方法です。

 <properties>
        <java.version>11</java.version>
        <spring.boot.mainClass>com.kiyotakeshi.non.web.Runner1</spring.boot.mainClass>
    </properties>
    <profiles>
        <profile>
            <id>2</id>
            <properties>
                <spring.boot.mainClass>com.kiyotakeshi.non.web.Runner2</spring.boot.mainClass>
            </properties>
        </profile>
        <profile>
            <id>3</id>
            <properties>
                <spring.boot.mainClass>com.kiyotakeshi.non.web.Runner3</spring.boot.mainClass>
            </properties>
        </profile>
    </profiles>

実行時に Profile を指定して実行します。

./mvnw spring-boot:run -P 2

参考にした stack overflow

Tomcat への war デプロイをコンテナで行う

今回の検証内容のリポジトリ

コンテナを利用して、手軽に WARファイル(.war) をデプロイして動作を検証する環境を作りました。

README.md に記載のように、以下の流れで .war の動作を検証できます。

コンテナを立ち上げて、

$ docker-compose up -d

$ docker-compose ps

任意の WARファイルをホストの /webapps ディレクトリにデプロイすることで、 コンテナにボリュームがマウントされているため、

    volumes: 
      - ./webapps:/usr/local/tomcat/webapps

Webアプリケーションの動作が確認できます。

$ curl http://localhost:8888/sample/
hello

Bean Lifcycle の確認2

前回の実装に加えて、ライフサイクル内でのカスタムの処理を確認します。

今回の検証内容のリポジトリ

メインメソッドはBean定義を行い、 Bean1 をDIコンテナから取得(ルックアップ)して使用した後に、DIコンテナを消去。 前回の検証と同様のものです。

public static void main(String[] args) {
    var context = new AnnotationConfigApplicationContext(AppConfig.class);

    Bean1 springBean1 = context.getBean(Bean1.class);
    springBean1.sayHello();

    context.close();
}

今回は、 Bean1 に関しては、コンフィギュレーションクラスでBeanを登録します。 その際に、 initMethod, destroyMehod を指定。

@Configuration
@ComponentScan
public class AppConfig {

    @Bean(initMethod = "initialization", destroyMethod = "destruction")
    public Bean1 springBean1() {
        return new Bean1();
    }
}

Bean1 は InitializingBean, DisposableBean を実装。 Bean定義にて指定した、 initialization,destruction メソッドを定義。

public class Bean1 implements InitializingBean, DisposableBean {

    private Bean2 Bean2;

    public Bean1() {
        System.out.println("\nCreating " + getClass().getSimpleName());
    }

    public void sayHello() {
        System.out.println("\nHello\n");
    }

    @Autowired
    public void setBean2(Bean2 bean2) {
        System.out.println("settingProperty on " + getClass().getSimpleName() + " to inject " + bean2.getClass().getSimpleName());
        this.Bean2 = bean2;
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("@PostConstruct " + getClass().getSimpleName());
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("@PreDestroy " + getClass().getSimpleName());
    }

    // InitializingBean のメソッド
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean::afterPropertiesSet " + getClass().getSimpleName());
    }

    // DisposableBean のメソッド
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean::destroy " + getClass().getSimpleName());
    }

    private void initialization() {
        System.out.println("@Bean(initMethod) " + getClass().getSimpleName());
    }

    private void destruction() {
        System.out.println("@Bean(destroyMethod) " + getClass().getSimpleName());
    }
}

メインメソッドを実行すると、Bean1 が依存している、 Bean2 のインスタンスが生成された後に、 Bean1 に関して、

  • コンストラク
  • セッターインジェクション
  • BeanPostProcessor の前処理(postProcessBeforeInitialization)
  • @PostConstruct
  • InitializingBean の afterPropertiesSet
  • Bean定義の initMethod
  • BeanPostProcessor の後処理(postProcessAfterInitialization)

が行われ、 sayHello() を実行した後に、DIコンテナを close するため、

  • @PreDestroy
  • DisposableBean の destroy
  • Bean定義の destroyMethod

が呼ばれます。

Creating Bean2
CustomBeanPostProcessor::postProcessBeforeInitialization Bean2 bean2
CustomBeanPostProcessor::postProcessAfterInitialization Bean2 bean2

Creating Bean1
settingProperty on Bean1 to inject Bean2
CustomBeanPostProcessor::postProcessBeforeInitialization Bean1 springBean1
@PostConstruct Bean1
InitializingBean::afterPropertiesSet Bean1
@Bean(initMethod) Bean1
CustomBeanPostProcessor::postProcessAfterInitialization Bean1 springBean1

Hello

@PreDestroy Bean1
DisposableBean::destroy Bean1
@Bean(destroyMethod) Bean1

Bean Lifcycle の確認

今回の検証内容のリポジトリ

Lifecycle の説明は こちら の記事が Spring の実装にも言及しており、わかりやすかった。

今回は、ライフサイクル内にカスタムの実装を行い挙動を確かめてみる。

まずは、コンフィギュレーションクラス(Bean定義を行うクラス)を用意。

@ComponentScan
@Configuration
public class AppConfig {

    @Bean
    public static CustomBeanFactoryPostProcessor getCustomBeanFactoryPostProcessor(){
        return new CustomBeanFactoryPostProcessor();
    }

    @Bean
    public static CustomBeanPostProcessor getCustomBeanPostProcessor(){
        return new CustomBeanPostProcessor();
    }
}

メインクラスは Bean(DIコンテナに登録されたコンポーネント) を取得(ルックアップ)するだけの処理。

public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            var bean1 = context.getBean(Bean1.class);
    }
}

最初にBean定義が読み込まれた後に、BeanFactoryPostProcessor による定義情報の書き換え処理が行われる。 ここでは、指定した文字列を含む Bean を表示している。

public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println(getClass().getSimpleName() + "::postProcessBeanFactory Listing Beans Start\n");

        Arrays.stream(beanFactory.getBeanDefinitionNames())
                .map(beanFactory::getBeanDefinition)
                .filter(beanDefinition -> beanClassNameContains(beanDefinition, "com.kiyotakeshi.beanLifecycle.beans"))
                .map(BeanDefinition::getBeanClassName)
                .forEach(System.out::println);

        System.out.println("\n" +getClass().getSimpleName() + "::postProcessBeanFactory Listing Beans End\n");
    }

    private boolean beanClassNameContains(BeanDefinition beanDefinition, String regex) {
        return beanDefinition != null && beanDefinition.getBeanClassName().contains(regex);
    }
}

次いで、Beanのインスタンスが生成され、インジェクション(コンストラクター -> フィールド -> セッター の順)が行われる。

以下のケースだと、 Bean1 のコンストラクタが呼ばれた後、 依存している Bean2 のインスタンスを作成するため Bean2 のコンストラクタが呼ばれる。

@Component
public class Bean1 {

    private Bean2 bean2;
    private Bean3 bean3;

    public Bean1() {
        System.out.println(getClass().getSimpleName() + "::constructor");
    }

    @Autowired
    public void setBean2(Bean2 bean2) {
        System.out.println(getClass().getSimpleName() + "::setSpringBean2");
        this.bean2 = bean2;
    }

    @Autowired
    public void setBean3(Bean3 bean3) {
        System.out.println(getClass().getSimpleName() + "::setSpringBean3");
        this.bean3 = bean3;
    }

    @PostConstruct
    public void init() {
        System.out.println(getClass().getSimpleName() + "::init");
    }

    @PreDestroy
    public void destroy() {
        System.out.println(getClass().getSimpleName() + "::destroy");
    }
}

Bean2 は依存しているものがないため、

@Component
public class Bean2 {

    public Bean2() {
        System.out.println(getClass().getSimpleName() + "::constructor");
    }

    @PostConstruct
    public void init() {
        System.out.println(getClass().getSimpleName() + "::init");
    }

    @PreDestroy
    public void destroy() {
        System.out.println(getClass().getSimpleName() + "::destroy");
    }
}

インジェクションが終わった後の BeanPostProcessor の前処理と @PostConstruct と BeanPostProcessor の後処理が行われる。

public class CustomBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(String.format("%s::postProcessBeforeInitialization %s %s", getClass().getSimpleName(), bean.getClass().getSimpleName(), beanName));
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(String.format("%s::postProcessAfterInitialization %s %s", getClass().getSimpleName(), bean.getClass().getSimpleName(), beanName));
        return bean;
    }
}

そして、 Bean1 において、 Bean2 のセッターインジェクションが行われる。 さらに Bean1 は Bean3 にも依存しているため同様の処理を行う。

すべての Beanがインスタンス化され、 メインクラスにて、 Bean1 をルックアップし、処理は終了となります。

try-with-resources を使っているため、最後に @PreDestroy が呼ばれます。

public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            var bean1 = context.getBean(Bean1.class);
    }
}

これは、 AnnotationConfigApplicationContext の親クラスである、 ConfigurableApplicationContext が Closeable(AutoCloseable) を継承しているためです。

// org/springframework/context/ConfigurableApplicationContext.java
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {

try-with-resources を使用しない場合は、 application context を .close() することで消去しないと、 @PreDestroy が呼ばれないです。

また、 .registerShutdownHook() も使用できます。 ※サンプルコード

ちなみに Spring Boot を使用する場合は、自動で行ってくれているので明示的に消去しなくてもよい。 ※サンプルコード

public static void main(String[] args) {
    var context = new AnnotationConfigApplicationContext(AppConfig.class);
    var bean1 = context.getBean(Bean1.class);
    context.close();
}

AnnotationConfigApplicationContext に profile をセットして使用する

今回の検証内容のリポジトリ

ComponentScan の設定を記載したクラスを用意。

@ComponentScan("com.diExperiment")
public class AppConfig {
}

他のクラスと依存している ProductPriceListReportService のコンストラクタ。 インターフェースである PriceListReport を引数にとるようにすることで結合度を下げる。

// コンストラクターインジェクション
public ProductPriceListReportService(ProductDao productDao, ProductPriceCalculator productPriceCalculator,PriceListReport priceListReport) {
    this.productDao = productDao;
    this.productPriceCalculator = productPriceCalculator;
    this.priceListReport = priceListReport;
}

とはいっても、DIコンテナを使わない場合は、 ProductPriceListReportService 作成時に、実装クラスを渡してあげることになる。

var productPriceListReportService = new ProductPriceListReportService(
        new ProductDao(),
        new ProductPriceCalculator(),
        // new PdfPriceListReport()
        new XlsPriceListReport()
);
productPriceListReportService.generateReport();

AnnotationConfigApplicationContext(DIコンテナ)を使う場合。

まずは、実装クラスに Profile を指定する。

@Component
@Profile("pdf-reports")
public class PdfPriceListReport implements PriceListReport {

    @Override
    public void writeReport(List<PriceList> priceLists) {
        System.out.println("Making Pdf Reports");
    }
}
@Component
@Profile("xls-reports")
public class XlsPriceListReport implements PriceListReport {

    @Override
    public void writeReport(List<PriceList> priceLists) {
        System.out.println("Making Xls Reports");
    }
}

Active Profiles をセットしてから、 AppConfig を登録することで、コンポーネントスキャンする際に、 Profile にて指定した実装クラスが Bean 登録される。

public static void main(String[] args) {

    AnnotationConfigApplicationContext context = getSpringContext("pdf-reports");
    // AnnotationConfigApplicationContext context = getSpringContext("xls-reports");
    var productPriceListReportService = context.getBean(ProductPriceListReportService.class);
    productPriceListReportService.generateReport();

    // 自分で登録した Bean を表示する
    System.out.println("\n--------- all custom defined beans in our applicationContext container ---------");
    String[] beanDefinitionNames = context.getBeanDefinitionNames();
    Arrays.stream(beanDefinitionNames)
            .filter(name -> !name.contains("org.springframework.context"))
            .forEach(System.out::println);

    context.close();
}

private static AnnotationConfigApplicationContext getSpringContext(String profile) {
    var context = new AnnotationConfigApplicationContext();
    context.getEnvironment().setActiveProfiles(profile);
    context.register(AppConfig.class);
    // context.scan("com.diExperiment"); // if you don't use AppConfig class
    context.refresh();
    return context;
}

実行結果。

Finding all product
Calculating product prices
Making Pdf Reports

--------- all custom defined beans in our applicationContext container ---------
appConfig
productPriceCalculator
productDao
pdfPriceListReport
productPriceListReportService