Configurationクラスの実行タイミング
@SpringBootApplicationと@Configurationはどちらが先に実行されるのか?
→@Configurationが先に実行される。
@SpringBootApplication
public class SpringTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringTestApplication.class, args);
System.out.println("SpringTestApplication");
}
}
@Configuration
public class AppConfig {
@Bean
public long create(){
System.out.println("AppConfigのcreate");
return 0L;
}
}
実行結果(コンソールの出力)
AppConfigのcreate
SpringTestApplication
@Configurationでnullを返すとUnsatisfiedDependencyExceptionになる。
@Configuration
public class AppConfig {
@Bean
public List<Articles> configMethod(){
return null;
}
}
@RestController
@RequestMapping(value = "/spring")
public class SpringTestRestController {
@Autowired
List<Articles> ArticlesList;
}
実行結果(コンソールの出力)
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springTestRestController': Unsatisfied dependency expressed through field 'ArticlesList':
@Autowiredの対象が同じ型で複数ある場合は、@Qualifierを使う。
DIするとき、データ型が同じのものが複数あると、起動時に以下のエラーになる。
Field numOfArticles in com.example.springTest.controller.SpringTestRestController required a single bean, but 2 were found:
@Configuration
public class AppConfig {
@Bean
public long configMethod1(){
return 1L;
}
@Bean
public long configMethod2(){
return 2L;
}
}
@RestController
@RequestMapping(value = "/spring")
public class SpringTestRestController {
@Autowired
long num;
}
解決策としては、@Beanに名前を付けて、参照先では@Qualifierを使う。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(name="bean1")
public long configMethod1(){
return 1L;
}
@Bean(name="bean2")
public long configMethod2(){
return 2L;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@RestController
@RequestMapping(value = "/spring")
public class SpringTestRestController {
@Autowired
@Qualifier("bean1")
long sample1;
@Autowired
@Qualifier("bean2")
long sample2;
}
Configurationクラスでも@Autowiredは使える。
なので、例えば@AutowiredでMyBatisのMapperを参照して、起動時にDBからレコードを取得するといったこともできる。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.springTest.mapper.ArticlesMapper;
@Configuration
public class AppConfig {
@Autowired
ArticlesMapper articlesMapper;
@Bean
public long configMethod1(){
long numOfArticles = articlesMapper.countByExample(new ArticlesExample());
return numOfArticles;
}
}
一方、@SpringBootApplicationのクラスでは@Autowiredでの参照はできない。
Cannot make a static reference to the non-static field
というコンパイルエラーになる。
当該インスタンスをmainメソッドから参照したい場合は、下記のようにすると良い。
public static void main(String[] args) {
ConfigurableApplicationContext appContext = SpringApplication.run(Application.class, args);
Sample sample = appContext.getBean(Sample.class);
sample.method();
}
Configurationクラスにメソッドが複数あった場合の実行順序は?
下記の感じで、@Configurationのクラスにメソッドが複数あった場合の実行順序の規則性は不明である。
少なくとも上から順ではないが、10回ほど実行してみたところ、いずれも同じ順序であったため、ランダムに実行されるというわけでもなさそう。が、試行母数がまだまだ少ないのでなんとも。。
@Configuration
public class AppConfig {
@Bean
public long configMethod0(){
}
@Bean
public String configMethod00(Environment env){
}
@Bean(name="bean1")
public long configMethod1(){
System.out.println("configMethod1");
return 1L;
}
@Bean(name="bean2")
public long configMethod2(){
System.out.println("configMethod2");
return 2L;
}
@Bean(name="bean3")
public long configMethod3(){
System.out.println("configMethod3");
return 2L;
}
@Bean(name="bean4")
public long configMethod4(){
System.out.println("configMethod4");
return 2L;
}
@Bean(name="bean5")
public long configMethod5(){
System.out.println("configMethod5");
return 2L;
}
}
Controllerクラスのメソッドで戻り値がない場合、HTTPステータスは200になる
ControllerクラスのメソッドでHTTPステータス:200を返すには、
return new ResponseEntity<>(HttpStatus.OK);
と書くことになる。以下のイメージである。
@RestController
@RequestMapping(value = "/spring")
public class SpringTestRestController {
@PostMapping(value = "/sample")
public ResponseEntity<Void> sample() {
return new ResponseEntity<>(HttpStatus.OK);
}
}
しかし、戻り値がなくとも、デフォルトとして200が返る。
@RestController
@RequestMapping(value = "/spring")
public class SpringTestRestController {
@PostMapping(value = "/sample")
public void sample() {
logger.info("access POST /sample");
}
}
return null;
としても同様に200が返る。
なお、
throw new Exception();
という感じで例外を投げれば500が返るが、
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
を使う方が良さそう。
application.propertiesの値を取得するには、@Valueを使うのがラク
例えばapplication.propertiesに下記の定義があるとする。
dirpath.list=/home/files_list.txt
この値は@Autowiredではなく@valueで取得することができる。
これによりDIコンテナにapplication.propertiesの値が注入される。
import org.springframework.beans.factory.annotation.Value;
@Value("${dirpath.list}")
String dirPath;
@valueを使う場合、DIコンテナに注入されるため、Spring Boot起動時に値が取得できないとエラーになる。
利点として、application.propertiesの記載漏れがあった場合にすぐ気づくことができる。
反面、多人数で製造をしている場合には、1人でもapplication.propertiesのコミット漏れや記述誤りがあると起動段階で弾かれるため、全員の作業に直接的な影響をもたらす側面はある。が、長期的に見ればプラスの効果だろう。
Spring BootのDIを使わず、ピュアなJavaの機能としてapplication.propertiesの値を取得する方法も当然ながらある。以下はResourceBundleで取得するものである。
こちらの場合、application.propertiesの記載漏れがあってもSpringBoot起動は成功し、getString()実行時にjava.util.MissingResourceExceptionが発生する。そのため単体テストでの確認観点が増えることになる。
import java.util.ResourceBundle;
public class クラス名 {
private static ResourceBundle resourceBundle = ResourceBundle.getBundle("application");
public void メソッド() {
System.out.println(resourceBundle.getString("キー"));
}
}
外部のapplication.propertiesを参照するようにしたい
maven packageなどでプロジェクトをjarファイルに固めると、application.propertiesもjarファイルに内包される。
しかし、これではapplication.propertiesの値を変更する度にjarを固め直さねばならず、運用が不便である。
起動引数に以下を指定することにより、jar内ではなく、外部の方のapplication.propertiesを読み込むようになる。
-Dspring.config.location="ファイルのパス/application.properties"
ただし、これが通用するのはDIを用いた場合(=@Valueを使う場合)の話であり、上述したResourceBundleの場合は変わらずjar内の方のapplication.propertiesが参照されるため、注意が必要である。基本的には使わない方が拡張性が高いだろう。
なお、余談としてmavenの話になるが、jar内に特定のファイルを含めたくない場合は、以下のようにpom.xmlにmaven-jar-pluginの設定を記述する。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<!-- ファイルを除く設定 -->
<excludes>
<exclude>application.properties</exclude>
</excludes>
</configuration>
</plugin>
【2024/08/15追記】
application.propertiesの読み込み方として、以下の様に、DTOと関連づける方法もある。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import lombok.Data;
@Data
@Configuration
@ConfigurationProperties(prefix = "dirpath")
public class FileDirConfig {
private String copy;
private String list;
}
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class ScheduledTasks {
@Autowired
FileDirConfig fileDirConfig;
public void method() {
System.out.println(fileDirConfig.getList());
System.out.println(fileDirConfig.getCopy());
}
}
この場合も、外部のapplication.propertiesを参照することはできるのだが、@PropertySource無指定だと、jar内(クラスパス内)のapplication.propertiesを先に参照し、その後外部のapplication.propertiesで値を上書きしている模様である。その証拠(?)として、(何度も検証してみたが)jar内にapplication.propertiesが含まれていないと以下のエラーが発生する。
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [com.example.dbinsert.DbinsertApplication]
中略
Caused by: java.io.FileNotFoundException: class path resource [application.properties] cannot be opened because it does not exist
jarにapplication.propertiesを内包せずに実行したいのであれば、@PropertySourceアノテーションにfile:hogehogeと指定する必要がある。(ただこれだと環境によってコードを変えないといけないので面倒。。)
@PropertySource("file:/home/user/conf/application.properties")
特定のメソッドを周期実行させたい
@Scheduledというアノテーションをメソッドに付与すると、そのメソッドは定期的に実行される。一定周期や、cron的な指定の仕方もできる。詳細はスケジュール機能を使ってみよう編を参照。
@SpringBootApplicationを付与したクラス(起動クラス)に@EnableSchedulingを付与することで、周期実行が有効化される(逆に言うと、@EnableSchedulingを付与しなければ周期実行は一切走らない)。
テストコード実行時にも周期実行が走ると挙動確認の邪魔になる。
test配下には@EnableSchedulingを外した起動クラスを設けることにより回避しよう。
コマンドアプリとして動かしたい
Spring FrameworkのDI機能やAOP機能を組み込んだコマンドラインアプリをつくることも可能である。
CommandLineRunnerもしくはApplicationRunnerを用いる。
(書き途中)
Webサーバの起動を無効化する方法
Spring Bootでは、組み込みサーバとしてデフォルトでTomcatが使用される。
依存関係にspring-boot-starter-webが含まれている場合、Spring Boot起動時、Tomcatが起動することになる。
この起動を無効にする方法は以下。
方法1:spring.main.web-application-type=none
application.propertiesに以下を追記する。
spring.main.web-application-type=none
方法2:setWebApplicationType(WebApplicationType.NONE)
@SpringBootApplicationのmainメソッドを以下の様にする。
SpringApplication springApplication = new SpringApplication(DemoApplication.class);
springApplication.setWebApplicationType(WebApplicationType.NONE);
ConfigurableApplicationContext appContext = springApplication.run(args);
依存関係にspring-boot-starter-webを含めるにも関わらずTomcatを無効にしたいというのは、Webアプリとコマンドアプリを1つのプロジェクトで同居させたい場合に使われることが多いだろう。
以下のコードのように、mainメソッドの引数が指定の条件に一致する場合にのみ、
builder.web(WebApplicationType.NONE);
とすることで、Webアプリケーションとして稼働中の間でも、別のプロセスで非Webアプリケーションとして起動することができる。
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
if (Arrays.asList(args).contains("disable-web")){
SpringApplicationBuilder builder = new SpringApplicationBuilder(SampleApplication.class);
builder.web(WebApplicationType.NONE);
builder.run(args);
// 処理終了
System.exit(0);
}
SpringApplication.run(SampleApplication.class, args);
}
Spring Boot終了時に処理を行いたい
Beanのライフサイクルは初期化段階、利用段階、終了段階の3つに分けることができる。
この内、終了段階の処理を記述するには、メソッドに@PreDestroyというアノテーションを付与する。
@PreDestroyアノテーションを付与したメソッドは、BeanがDIコンテナ内で破棄される直前に呼び出される。
例えば、Spring Bootを終了する際、関連するプロセスも併せてkillする、などの対応が可能になる。
おまけ:設計的な話
マスタテーブルや設定ファイルの値を参照する処理が各サービスクラスにあると、その都度テーブル参照やファイル読み込みが発生し、エラーハンドリングが煩雑になる。
起動時にDIコンテナに登録するようにしておいた方がスッキリとした設計になる。
大人数で開発するプロジェクトだと、機能ごとに担当が割り振られ、こうしたプロジェクト構成の考慮がおざなりになる場合があるが、本当にアンチパターンだと思います。