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.xml の spring-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
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