目次 |
---|
初めに |
循環参照エラーについて |
DIやDIコンテナについて |
初めに
これは、Spring Securityを使用したログイン機能の実装を勉強していた時に発生しました。以下のサイトを見ると似たようなところで躓いている人が多いようです(私もその一人(〃ノωノ))。今回は実際に自分が起こした循環参照のエラーを例にその対処法について解説したいと思います。
循環参照エラーについて
循環参照は特定のオブジェクトやクラスが相互に依存する状態を指し、この状態が発生するとアプリケーションの動作に問題(アプリの起動に失敗など)が生じることが多いです。具体的には、AクラスがBクラスに依存し、BクラスがAクラスに依存しているような状態のことを指します。循環参照エラーの原因をちゃんと理解するには、DIやDIコンテナについてちゃんと理解する必要がございます。DIやDIコンテナについての解説も記載したの参考にしてください。
目次 |
---|
循環参照の概要 |
循環参照エラーの実例 |
今回の循環参照エラーの原因 |
循環参照エラーの対処方法 |
循環参照の概要
循環参照は、オブジェクトの相互依存関係が複雑に絡み合い、互いに参照することで無限ループ状態や予期せぬエラーを引き起こす状態を指します。これは特にDIコンテナを使用したフレームワークやアプリケーションで問題となることが多いです。循環参照の問題点としては、アプリケーションの起動時にDIコンテナがオブジェクトの依存関係を解決しようとした際に、循環参照を検出してエラーをスローすることが挙げられます。これにより、アプリケーションの起動が阻害されることがあります。循環参照エラーの実例
循環参照エラーの実例について紹介します。最初にも紹介したように、多くの開発者がSpring Securityを使用したログイン機能の実装時に循環参照エラーに遭遇しています。私自身も、Spring Securityを使用してログイン機能を実装した際にこのエラーに直面しました。具体的には、「データベースからパスワードを取得し、それを比較する機能」を実装し、動作確認を行ったところ、循環参照エラーが発生しました。このようなエラーの背後には、特定のサービスやコンポーネントが互いに依存する構造が潜んでいることが多いです。Spring Securityのような複雑なフレームワークを使用する際には、特に注意が必要です。この状態で起動すると以下のようなエラーログがコンソール出力されます。
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-09-21T18:32:20.597+09:00 ERROR 8408 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| customAuthenticationProvider (field private org.springframework.security.crypto.password.PasswordEncoder com.app.login.configurations.CustomAuthenticationProvider.passwordEncoder)
↑ ↓
| securityConfig (field private com.app.login.configurations.CustomAuthenticationProvider com.app.login.configurations.SecurityConfig.customAuthenticationProvider)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
今回の循環参照エラーの原因
循環参照エラーが発生した原因は、以下の2つの設定に起因しています。-
SecurityConfig: このクラスでは
CustomAuthenticationProvider
(カスタマイズした認証クラス)をDI(依存性注入)しており、このプロバイダはユーザーの認証処理を実施しています。 -
PasswordEncoderのBean登録:
SecurityConfig
内で、PasswordEncoder
をBeanとして登録しています。これにより、パスワードのハッシュ化および比較のエンコーダーを提供しています。
以下は問題となったソースコードの詳細です。
こちらは、SecurityConfig
になります。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// カスタム認証プロバイダを設定
.authenticationProvider(customAuthenticationProvider)
// CORSの設定を適用
.cors(customizer -> customizer.configurationSource(corsConfigurationSource()))
// CSRFの保護を無効にする
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
// loginのパスへのリクエストはすべて許可
.requestMatchers("/login").permitAll()
// その他のリクエストは認証が必要
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
// ログイン処理のURLを指定(フロントがログインボタン実行時にPOSTする場所)
.loginProcessingUrl("/login")
// カスタムログインページのURLを指定(Spring Securityデフォルトの画面を置き換える)
.loginPage("http://127.0.0.1:5500/view/login.html")
// ログイン成功時のリダイレクト先URLを指定
.defaultSuccessUrl("http://127.0.0.1:5500/view/index.html")
// 認証失敗時のリダイレクト先URLを指定
.failureUrl("http://127.0.0.1:5500/view/error.html")
);
return http.build();
}
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
// CORSの設定を行うためのオブジェクトを生成
CorsConfiguration configuration = new CorsConfiguration();
// クレデンシャル(資格情報(CookieやHTTP認証情報))を含むリクエストを許可する
configuration.setAllowCredentials(true);
// 許可するオリジン(この場合は"http://127.0.0.1:5500"のみ)を設定
configuration.addAllowedOrigin("http://127.0.0.1:5500");
// 任意のヘッダーを許可
configuration.addAllowedHeader("*");
// 任意のHTTPメソッド(GET, POSTなど)を許可
configuration.addAllowedMethod("*");
// CORS設定をURLベースで行うためのオブジェクトを生成
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 全てのURLパスにこのCORS設定を適用
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
こちらは、CustomAuthenticationProvider
になります。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.app.login.service.CustomUserDetailsService;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String providedPassword = (String) authentication.getCredentials();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(providedPassword, userDetails.getPassword())) {
return new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());
} else {
throw new BadCredentialsException("Authentication failed");
}
}
@Override
public boolean supports(Class<?> authentication) {
// authentication(認証方式)がUsernamePasswordAuthenticationToken.class(ユーザー名とパスワード認証)か判定
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
上記のソースコードを確認すると以下の内容が原因で循環参照エラー発生しています。
-
SecurityConfig
では、CustomAuthenticationProvider
をDIしています。 -
CustomAuthenticationProvider
では、PasswordEncoder
をDIしています。 - この
PasswordEncoder
はSecurityConfig
でDIコンテナにBean登録しています。
この状態が、SecurityConfig
とCustomAuthenticationProvider
の間で循環の依存関係を作り出しています。具体的には、SecurityConfig
がCustomAuthenticationProvider
を参照し、そのCustomAuthenticationProvider
が再びSecurityConfig
で登録されたPasswordEncoder
を参照しているため、この循環がエラーの原因となっています。
循環参照エラーの対処方法
今回の場合の循環参照エラーの対処方法は3つあります。
- 遅延ローディング:@Lazyアノテーションを使用して、SpringにBeanの初期化を遅らせるように指示します。
この方法は実装が簡単ですが、Beanの初期化順序を遅らせるだけであり、Bean登録が増えるほど制御が困難になる可能性があります。クラス間の依存性が強い場合、この方法を推奨するのは難しいです。
- コンストラクタ・メソッドインジェクション:コンストラクタやメソッドの引数にBean登録したクラスを追加することで、明示的な依存関係の注入を行います。
コンストラクタインジェクションを使用すると、Beanの初期化の際に必要なすべての依存関係が解決されるまでインスタンス化が遅延されます。循環参照が存在する場合、Springはこの問題を初期化時に検出します。この方法は、循環参照を明確に検出する点で有効ですが、クラス間の依存性が高い場合、その設計自体を見直すべきかもしれません。
- Bean登録の分割:新しいクラスを作成し、依存関係を再構築することで循環参照を防ぎます。
この方法は他の2つに比べて実装が複雑になるかもしれませんが、依存関係の削減が期待できます。
遅延ローディングを利用した修正方法
遅延ローディングを利用する修正方法として、SecurityConfig
でCustomAuthenticationProvider
をDIする部分に@Lazyアノテーションを追加します。具体的な変更箇所は以下の通りです。
// CustomAuthenticationProvider Beanをこのクラスに注入する
@Autowired
+ @Lazy
private CustomAuthenticationProvider customAuthenticationProvider;
ソースコード全体は以下のようになります。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// CustomAuthenticationProvider Beanをこのクラスに注入する
@Autowired
@Lazy
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// カスタム認証プロバイダを設定
.authenticationProvider(customAuthenticationProvider)
// CORSの設定を適用
.cors(customizer -> customizer.configurationSource(corsConfigurationSource()))
// CSRFの保護を無効にする
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
// loginのパスへのリクエストはすべて許可
.requestMatchers("/login").permitAll()
// その他のリクエストは認証が必要
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
// ログイン処理のURLを指定(フロントがログインボタン実行時にPOSTする場所)
.loginProcessingUrl("/login")
// カスタムログインページのURLを指定(Spring Securityデフォルトの画面を置き換える)
.loginPage("http://127.0.0.1:5500/view/login.html")
// ログイン成功時のリダイレクト先URLを指定
.defaultSuccessUrl("http://127.0.0.1:5500/view/index.html")
// 認証失敗時のリダイレクト先URLを指定
.failureUrl("http://127.0.0.1:5500/view/error.html")
);
return http.build();
}
// @Beanをつけることで、このメソッドがSpringのコンテナにBeanとして登録される
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
// CORSの設定を行うためのオブジェクトを生成
CorsConfiguration configuration = new CorsConfiguration();
// クレデンシャル(資格情報(CookieやHTTP認証情報))を含むリクエストを許可する
configuration.setAllowCredentials(true);
// 許可するオリジン(この場合は"http://127.0.0.1:5500"のみ)を設定
configuration.addAllowedOrigin("http://127.0.0.1:5500");
// 任意のヘッダーを許可
configuration.addAllowedHeader("*");
// 任意のHTTPメソッド(GET, POSTなど)を許可
configuration.addAllowedMethod("*");
// CORS設定をURLベースで行うためのオブジェクトを生成
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 全てのURLパスにこのCORS設定を適用
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
コンストラクタ・メソッドインジェクションを利用した修正方法
コンストラクタ・メソッドインジェクションを利用する修正方法は以下の通りです。
-
SecurityConfig
でCustomAuthenticationProvider
をフィールドインジェクションしていた個所を削除します。 - 次に
securityFilterChain
メソッドの引数に、CustomAuthenticationProvider
を追加します。
具体的な変更箇所は以下の通りです。
// 以下を削除
- @Autowired
- @Lazy
- private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthenticationProvider customAuthenticationProvider) throws Exception {
ソースコード全体は以下のようになります。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthenticationProvider customAuthenticationProvider) throws Exception {
http
// カスタム認証プロバイダを設定
.authenticationProvider(customAuthenticationProvider)
// CORSの設定を適用
.cors(customizer -> customizer.configurationSource(corsConfigurationSource()))
// CSRFの保護を無効にする
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
// loginのパスへのリクエストはすべて許可
.requestMatchers("/login").permitAll()
// その他のリクエストは認証が必要
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
// ログイン処理のURLを指定(フロントがログインボタン実行時にPOSTする場所)
.loginProcessingUrl("/login")
// カスタムログインページのURLを指定(Spring Securityデフォルトの画面を置き換える)
.loginPage("http://127.0.0.1:5500/view/login.html")
// ログイン成功時のリダイレクト先URLを指定
.defaultSuccessUrl("http://127.0.0.1:5500/view/index.html")
// 認証失敗時のリダイレクト先URLを指定
.failureUrl("http://127.0.0.1:5500/view/error.html")
);
return http.build();
}
// @Beanをつけることで、このメソッドがSpringのコンテナにBeanとして登録される
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
// CORSの設定を行うためのオブジェクトを生成
CorsConfiguration configuration = new CorsConfiguration();
// クレデンシャル(資格情報(CookieやHTTP認証情報))を含むリクエストを許可する
configuration.setAllowCredentials(true);
// 許可するオリジン(この場合は"http://127.0.0.1:5500"のみ)を設定
configuration.addAllowedOrigin("http://127.0.0.1:5500");
// 任意のヘッダーを許可
configuration.addAllowedHeader("*");
// 任意のHTTPメソッド(GET, POSTなど)を許可
configuration.addAllowedMethod("*");
// CORS設定をURLベースで行うためのオブジェクトを生成
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 全てのURLパスにこのCORS設定を適用
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Bean登録の分割を利用した修正方法
Bean登録の分割を利用する修正方法は以下の通りです。
-
SecurityConfig
でCustomAuthenticationProvider
をフィールドインジェクションしていた個所をコンストラクタインジェクションに変更します。
フィールドインジェクションとは、クラスのフィールド(変数)に直接、必要なオブジェクトやコンポーネントを注入することを指します。しかし、フィールドインジェクションの利用することは非推奨とされています。
-
SecurityConfig
で実装していたPasswordEncoder
のBean登録処理を削除します。 -
新しく
PasswordEncoderConfig
クラスを作成し、2で削除したPasswordEncoder
のBean登録処理を実装します。 -
CustomAuthenticationProvider
のフィールドインジェクションしていた個所をコンストラクタインジェクションに変更します。
具体的な修正箇所は以下の通りです。
- @Autowired
- private CustomAuthenticationProvider customAuthenticationProvider;
+ private final CustomAuthenticationProvider customAuthenticationProvider;
+ public SecurityConfig(CustomAuthenticationProvider customAuthenticationProvider) {
+ this.customAuthenticationProvider = customAuthenticationProvider;
+ }
// securityFilterChainとcorsConfigurationSourceメソッド内容は省略
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
- @Autowired
- private CustomUserDetailsService userDetailsService;
- @Autowired
- private PasswordEncoder passwordEncoder;
+ private final CustomUserDetailsService userDetailsService;
+ private final PasswordEncoder passwordEncoder;
+ public CustomAuthenticationProvider(
+ CustomUserDetailsService userDetailsService,
+ PasswordEncoder passwordEncoder
+ ) {
+ this.userDetailsService = userDetailsService;
+ this.passwordEncoder = passwordEncoder;
+ }
PasswordEncoderをBean登録する新規クラスPasswordEncoderConfig
の内容です。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
ソースコード全体は以下のようになります。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomAuthenticationProvider customAuthenticationProvider;
public SecurityConfig(CustomAuthenticationProvider customAuthenticationProvider) {
this.customAuthenticationProvider = customAuthenticationProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// カスタム認証プロバイダを設定
.authenticationProvider(customAuthenticationProvider)
// CORSの設定を適用
.cors(customizer -> customizer.configurationSource(corsConfigurationSource()))
// CSRFの保護を無効にする
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
// loginのパスへのリクエストはすべて許可
.requestMatchers("/login").permitAll()
// その他のリクエストは認証が必要
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
// ログイン処理のURLを指定(フロントがログインボタン実行時にPOSTする場所)
.loginProcessingUrl("/login")
// カスタムログインページのURLを指定(Spring Securityデフォルトの画面を置き換える)
.loginPage("http://127.0.0.1:5500/view/login.html")
// ログイン成功時のリダイレクト先URLを指定
.defaultSuccessUrl("http://127.0.0.1:5500/view/index.html")
// 認証失敗時のリダイレクト先URLを指定
.failureUrl("http://127.0.0.1:5500/view/error.html")
);
return http.build();
}
// @Beanをつけることで、このメソッドがSpringのコンテナにBeanとして登録される
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
// CORSの設定を行うためのオブジェクトを生成
CorsConfiguration configuration = new CorsConfiguration();
// クレデンシャル(資格情報(CookieやHTTP認証情報))を含むリクエストを許可する
configuration.setAllowCredentials(true);
// 許可するオリジン(この場合は"http://127.0.0.1:5500"のみ)を設定
configuration.addAllowedOrigin("http://127.0.0.1:5500");
// 任意のヘッダーを許可
configuration.addAllowedHeader("*");
// 任意のHTTPメソッド(GET, POSTなど)を許可
configuration.addAllowedMethod("*");
// CORS設定をURLベースで行うためのオブジェクトを生成
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 全てのURLパスにこのCORS設定を適用
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.app.login.service.CustomUserDetailsService;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public CustomAuthenticationProvider(
CustomUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder
) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String providedPassword = (String) authentication.getCredentials();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(providedPassword, userDetails.getPassword())) {
return new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());
} else {
throw new BadCredentialsException("Authentication failed");
}
}
@Override
public boolean supports(Class<?> authentication) {
// authentication(認証方式)がUsernamePasswordAuthenticationToken.class(ユーザー名とパスワード認証)か判定
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
ここで、Spring Securityでログイン機能の実装中に起きた循環参照エラーについての対象方法についての説明は終わりです。以下は、DIやDIコンテナについての解説です。
DIやDIコンテナについて
循環参照エラーの解消にはdiやdiコンテナの仕組みをちゃんと理解する必要があります。今まで、なんとなくで実装していた方はまずはここを見て復習してみてください。(私もその一人(〃ノωノ))
DIとは何か
Springを勉強すればあらゆる本やネット記事、動画で以下の説明を見聞きすることでしょう。- DI(Dependency Injection):依存性の注入
そして「(。・ω・。)?」となり、なんとなく
「依存性の注入を心掛けた実装をしないといけないのか、へー興味ない(-‸ლ)」となることでしょう(笑)。
しかしこの意味を理解しないとSpring DIを理解するのは難しいです。なのでこの言葉をもう少し意味のわかる言葉に変換したいと思います。
- 依存性の注入:外部からオブジェクトを注入することでクラス同士の依存性を弱める
しかし、個人的な感想としては、依存性の注入という言葉だけでは、実際には依存性が増加しているようなイメージを持つかもしれません。この点を鑑みると、DEI(Dependency External Injection)という名前のほうが、もう少し直感的に捉えやすくするかもしれません。日本語で直訳すると「依存性の外部注入」となり、外部からの依存関係の注入という意味がもう少し明確になるでしょう。この点についてはあくまで私の個人的な考えですが、DIの本質とメリットを理解する上での視点として参考にしていただければと思います。
とはいえ言葉だけでは難しいと思うので実際のソースコードでもを確認しましょう。
以下のHelloメッセージを表示する機能を依存性の注入のなしの場合と依存性の注入のありの場合で呼び出すクラスを作成します。
public class HelloPrint {
void helloPrint(String str) {
System.out.println("Hello "+ str +" DI!");
}
}
まずは、依存性の注入なしのソースコードです。
public class HelloNoDI {
private HelloPrint helloPrint;
HelloNoDI(){
this.helloPrint = new HelloPrint();
}
void run() {
String str = "No";
this.helloPrint.helloPrint(str);
}
public static void main(String args[]){
HelloNoDI helloNoDI = new HelloNoDI();
helloNoDI.run();
}
}
見てもこれだけじゃよくわからないですよね。
次のコードが、依存性の注入ありのソースコードです。
public class HelloDI {
private HelloPrint helloPrint;
HelloDI(HelloPrint helloPrint){
this.helloPrint = helloPrint;
}
void run() {
String str = "Yes";
this.helloPrint.helloPrint(str);
}
public static void main(String args[]){
HelloPrint helloPrint = new HelloPrint();
HelloDI helloDI = new HelloDI(helloPrint);
helloDI.run();
}
}
まず一つ目の違いは、コンストラクタの引数の有無です。依存性の注入なしの場合は引数がなくて、依存性の注入ありの場合は、引数があります。また、依存性のなしの場合はHelloPrint
のインスタンスの生成をコンストラクタ内で行っていますが依存性のありの場合は、コンストラクタの引数を代入しています。
// 依存性の注入なし
- HelloNoDI(){
- this.helloPrint = new HelloPrint();
- }
// 依存性の注入あり
+ HelloDI(HelloPrint helloPrint){
+ this.helloPrint = helloPrint;
+ }
二つ目の違いは、コンストラクタ呼び出し前のHelloPrint
のインスタンスの生成の有無です。依存性のなしの場合は何もせず、依存性のありの場合は、HelloPrint
のインスタンスの生成を行って、それをコンストラクタの引数に追加して呼び出しています。
// 依存性の注入なし
- public static void main(String args[]){
- HelloNoDI helloNoDI = new HelloNoDI();
- helloNoDI.run();
- }
// 依存性の注入あり
+ public static void main(String args[]){
+ HelloPrint helloPrint = new HelloPrint();
+ HelloDI helloDI = new HelloDI(helloPrint);
+ helloDI.run();
+ }
これを見ると依存性の注入ありの場合はmainメソッド(今回の外部にあたるもの)からオブジェクト(HelloPrint
のインスタンス)をグローバル変数のhelloPrint に注入(コンストラクタの引数を介して代入)しています。
一方で、依存性の注入なしの場合は、コンストラクタ内でオブジェクト(HelloPrint
のインスタンス)を注入(代入)しているので、クラス同士(HelloNoDI
とHelloPrint
)の依存性(結びつき)が強いと言えます。
以上がDIの説明です。コードがシンプルなのであまり違いがわからないと思いますが、依存性の注入ありで実装するとソースコードの再利用性や拡張性が向上します。また、外部からのオブジェクトの注入が可能になるため、テスト時にモックオブジェクトなどを用いることも容易になります。
DIコンテナとは何か
DIコンテナには以下のような役割があります。- Beanの登録:アノテーションや設定ファイルを通じて、オブジェクトをDIコンテナに登録することができます。これにより、アプリケーション内でそのオブジェクトが必要な場面で自動的に提供されるようになります。
使用するアノテーション例:@Component, @Bean, @Configuration, @Service, @Repository, @Controller
- 依存性の注入の自動化:開発者は、インスタンスを手動で生成やクラスに代入するといった処理を書く必要がなくなります。アノテーションを付与したり、メソッドの引数に型を指定するだけで、DIコンテナが適切なオブジェクトを自動で注入してくれます。
使用するアノテーション例:@Autowired, @Inject
- Beanのライフサイクルの管理:DIコンテナは、Beanの生成、初期化、破棄などのBeanのライフサイクルを管理します。例えば、アプリケーションの開始時に特定の動作を行うための初期化メソッドや、アプリケーションの終了時にリソースを解放するための終了メソッドを指定することができます。
使用するアノテーション例:@PostConstruct, @PreDestroy
- 設定ファイルの一元管理:アプリケーションの設定情報やBeanの定義などを一元的に管理することができます。これにより、設定変更や機能追加を行う際の変更範囲を最小限に抑えることができます。
Beanの登録
Springでは、アプリケーションのコンポーネント(部品や機能)をBeanとして管理します。Beanとは、Springの中で管理されるオブジェクトやクラスのことを指します。これらのBeanは、対象のクラスやメソッドに特定のアノテーションを付与したり、XMLの設定ファイルに記述すること(古いバージョン)でSpringのDIコンテナ(保管庫)に登録され、アプリケーションがそれを必要とする時に提供されます。
図にすると以下のような感じです。
以下はHelloPrint
をDIコンテナに登録するサンプルコードです。
import org.springframework.stereotype.Component;
@Component
public class HelloPrint {
void helloPrint(String str) {
System.out.println("Hello "+ str +" DI!");
}
}
古いバージョンのSpringだとXMLで記述するかもしれません。
<beans>
<bean id="helloPrint" class="com.example.HelloPrint"/>
</beans>
依存性の注入の自動化
依存性の注入の自動化は先ほどの図の「DI(依存性の注入)」の部分に当たります。
以下はHelloPrint
の依存性の注入を自動化したサンプルコードです。このコードではHelloService
クラスのhelloPrintフィールドにHelloPrint
のインスタンスを生成るコードが不要で「this.helloPrint」にオブジェクトが自動的に注入されることを確認できます。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
private final HelloPrint helloPrint;
@Autowired
public HelloService(HelloPrint helloPrint) {
this.helloPrint = helloPrint;
}
public void greet(String str) {
helloPrint.helloPrint(str);
}
}
このサンプルコードでは、コンストラクタが1つだけの場合、@Autowiredアノテーションは省略可能です。
依存性の注入を自動化するためには、対象クラスもBeanとして登録されている必要があります。今回の例では、@ServiceアノテーションによってBeanとして登録されています。
Beanのライフサイクルの管理
SpringのBeanのライフサイクルは、SpringのコンテナがBeanを管理する際のライフサイクルを意味します。ライフサイクルとは、Beanのインスタンス化、依存性の注入、初期化、使用、破棄などがあげられます。
以下はライフサイクルを示すサンプルコードです。
@PostConstructはBeanの初期化時(アプリ起動時)、@PreDestroyはBeanの破棄時(アプリ終了時)に呼ばれるメソッドを指定するためのアノテーションです。
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Component
public class LifecycleBean {
@PostConstruct
public void init() {
System.out.println("LifecycleBean is initialized.");
}
@PreDestroy
public void destroy() {
System.out.println("LifecycleBean is about to be destroyed.");
}
}
以下は、起動時のコンソール出力の結果です。この出力は、LifecycleBean
が初期化されたことを示しています。
以下のメッセージが出力がされていると思います。
LifecycleBean is initialized.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.3)
2023-09-21T15:23:17.831+09:00 INFO 4940 --- [ restartedMain] com.app.login.LoginApplication
: Starting LoginApplication using Java 17.0.4.1 with PID 4940 (C:\Java\workspace\Login\login\bin\main started by takeru in C:\Java\workspace\Login)
2023-09-21T15:23:17.835+09:00 INFO 4940 --- [ restartedMain] com.app.login.LoginApplication
: No active profile set, falling back to 1 default profile: "default"
2023-09-21T15:23:17.941+09:00 INFO 4940 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor
: Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-09-21T15:23:17.943+09:00 INFO 4940 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor
: For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-09-21T15:23:20.916+09:00 INFO 4940 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer
: Tomcat initialized with port(s): 8080 (http)
2023-09-21T15:23:20.937+09:00 INFO 4940 --- [ restartedMain] o.apache.catalina.core.StandardService
: Starting service [Tomcat]
2023-09-21T15:23:20.939+09:00 INFO 4940 --- [ restartedMain] o.apache.catalina.core.StandardEngine
: Starting Servlet engine: [Apache Tomcat/10.1.12]
2023-09-21T15:23:21.082+09:00 INFO 4940 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]
: Initializing Spring embedded WebApplicationContext
2023-09-21T15:23:21.086+09:00 INFO 4940 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext
: Root WebApplicationContext: initialization completed in 3139 ms
+ LifecycleBean is initialized.
2023-09-21T15:23:22.129+09:00 INFO 4940 --- [ restartedMain] o.s.s.web.DefaultSecurityFilterChain
: Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@32a7c477, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7e650ece, org.springframework.security.web.context.SecurityContextHolderFilter@6c35240a, org.springframework.security.web.header.HeaderWriterFilter@56095e54, org.springframework.web.filter.CorsFilter@4eb72c9e, org.springframework.security.web.authentication.logout.LogoutFilter@51d1b33c, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@443deafc, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2f863f1, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@7fb314c1, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5ef1aaff, org.springframework.security.web.access.ExceptionTranslationFilter@3fe379d3, org.springframework.security.web.access.intercept.AuthorizationFilter@1d1b5b7c]
2023-09-21T15:23:22.513+09:00 INFO 4940 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is
running on port 35729
2023-09-21T15:23:22.586+09:00 INFO 4940 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-21T15:23:22.604+09:00 INFO 4940 --- [ restartedMain] com.app.login.LoginApplication : Started LoginApplication in 5.521 seconds (process running for 6.32)
以下は、終了時のコンソール出力の結果です。この出力は、LifecycleBean
が破棄される直前のことを示しています。 最後の行に以下のメッセージが出力がされていると思います。
最後の行に以下のメッセージが出力がされていると思います。
LifecycleBean is about to be destroyed.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.3)
2023-09-21T15:23:17.831+09:00 INFO 4940 --- [ restartedMain] com.app.login.LoginApplication
: Starting LoginApplication using Java 17.0.4.1 with PID 4940 (C:\Java\workspace\Login\login\bin\main started by takeru in C:\Java\workspace\Login)
2023-09-21T15:23:17.835+09:00 INFO 4940 --- [ restartedMain] com.app.login.LoginApplication
: No active profile set, falling back to 1 default profile: "default"
2023-09-21T15:23:17.941+09:00 INFO 4940 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor
: Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-09-21T15:23:17.943+09:00 INFO 4940 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor
: For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-09-21T15:23:20.916+09:00 INFO 4940 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer
: Tomcat initialized with port(s): 8080 (http)
2023-09-21T15:23:20.937+09:00 INFO 4940 --- [ restartedMain] o.apache.catalina.core.StandardService
: Starting service [Tomcat]
2023-09-21T15:23:20.939+09:00 INFO 4940 --- [ restartedMain] o.apache.catalina.core.StandardEngine
: Starting Servlet engine: [Apache Tomcat/10.1.12]
2023-09-21T15:23:21.082+09:00 INFO 4940 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]
: Initializing Spring embedded WebApplicationContext
2023-09-21T15:23:21.086+09:00 INFO 4940 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext
: Root WebApplicationContext: initialization completed in 3139 ms
LifecycleBean is initialized.
2023-09-21T15:23:22.129+09:00 INFO 4940 --- [ restartedMain] o.s.s.web.DefaultSecurityFilterChain
: Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@32a7c477, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7e650ece, org.springframework.security.web.context.SecurityContextHolderFilter@6c35240a, org.springframework.security.web.header.HeaderWriterFilter@56095e54, org.springframework.web.filter.CorsFilter@4eb72c9e, org.springframework.security.web.authentication.logout.LogoutFilter@51d1b33c, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@443deafc, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2f863f1, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@7fb314c1, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5ef1aaff, org.springframework.security.web.access.ExceptionTranslationFilter@3fe379d3, org.springframework.security.web.access.intercept.AuthorizationFilter@1d1b5b7c]
2023-09-21T15:23:22.513+09:00 INFO 4940 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is
running on port 35729
2023-09-21T15:23:22.586+09:00 INFO 4940 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-21T15:23:22.604+09:00 INFO 4940 --- [ restartedMain] com.app.login.LoginApplication : Started LoginApplication in 5.521 seconds (process running for 6.32)
+ LifecycleBean is about to be destroyed.
設定ファイルの一元管理
設定ファイルを一元管理することで、システムの設定やメッセージなどの情報を一箇所にまとめることができます。このアプローチにより、変更が必要な場合や設定値を参照する際に、該当の情報を簡単に探し出しやすくなります。
以下は、LifecycleBean
の初期化時と破棄時メッセージをapplication.propertiesに外部化しました。
app.init.message=LifecycleBean is initialized.
app.destroy.message=LifecycleBean is about to be destroyed.
以下はapplication.propertiesに設定したメッセージを取得するサンプルコードです。「@Valueアノテーション」を使用することで設定ファイルの値をJavaで取得できるようになります。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConfigurationSampleBean {
@Value("${app.init.message}")
private String initMessage;
@Value("${app.destroy.message}")
private String destroyMessage;
public String getInitMessage() {
return this.initMessage;
}
public String getDestroyMessage() {
return this.destroyMessage;
}
}
LifecycleBean
で設定していたメッセージをConfigurationSampleBean
から取得するように修正しています。
package com.app.login.configurations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Component
public class LifecycleBean {
private ConfigurationSampleBean configurationSampleBean;
LifecycleBean(ConfigurationSampleBean configurationSampleBean){
this.configurationSampleBean = configurationSampleBean;
}
@PostConstruct
public void init() {
System.out.println(this.configurationSampleBean.getInitMessage());
}
@PreDestroy
public void destroy() {
System.out.println(this.configurationSampleBean.getDestroyMessage());
}
}
以下は、起動時のコンソール出力の結果です。この出力は、LifecycleBean
が初期化されたことを示しています。
以下のメッセージが出力がされていると思います。
LifecycleBean is initialized.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.3)
2023-09-21T16:16:19.197+09:00 INFO 6448 --- [ restartedMain] com.app.login.LoginApplication : Starting LoginApplication using Java 17.0.4.1 with PID 6448 (C:\Java\workspace\Login\login\bin\main started by takeru in C:\Java\workspace\Login)
2023-09-21T16:16:19.201+09:00 INFO 6448 --- [ restartedMain] com.app.login.LoginApplication : No active profile set, falling back to 1 default profile: "default"
2023-09-21T16:16:19.312+09:00 INFO 6448 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-09-21T16:16:19.313+09:00 INFO 6448 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-09-21T16:16:22.075+09:00 INFO 6448 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-09-21T16:16:22.118+09:00 INFO 6448 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-09-21T16:16:22.123+09:00 INFO 6448 --- [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.12]
2023-09-21T16:16:22.312+09:00 INFO 6448 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-09-21T16:16:22.318+09:00 INFO 6448 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 3000 ms
+ LifecycleBean is initialized.
2023-09-21T16:16:23.425+09:00 INFO 6448 --- [ restartedMain] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@37a46c54, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@e52b243, org.springframework.security.web.context.SecurityContextHolderFilter@4bc2ec5, org.springframework.security.web.header.HeaderWriterFilter@4eb72c9e, org.springframework.web.filter.CorsFilter@e2e6051, org.springframework.security.web.authentication.logout.LogoutFilter@6455a2a4, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4429b5d4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@206dcbce, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@213502ab, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@73abefcc, org.springframework.security.web.access.ExceptionTranslationFilter@32a7c477, org.springframework.security.web.access.intercept.AuthorizationFilter@1ad34c99]
2023-09-21T16:16:23.811+09:00 INFO 6448 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is
running on port 35729
2023-09-21T16:16:23.909+09:00 INFO 6448 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-21T16:16:23.927+09:00 INFO 6448 --- [ restartedMain] com.app.login.LoginApplication : Started LoginApplication in 5.425 seconds (process running for 6.112)
以下は、終了時のコンソール出力の結果です。この出力は、LifecycleBean
が破棄される直前のことを示しています。
最後の行に以下のメッセージが出力がされていると思います。
LifecycleBean is about to be destroyed.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.3)
2023-09-21T16:16:19.197+09:00 INFO 6448 --- [ restartedMain] com.app.login.LoginApplication : Starting LoginApplication using Java 17.0.4.1 with PID 6448 (C:\Java\workspace\Login\login\bin\main started by takeru in C:\Java\workspace\Login)
2023-09-21T16:16:19.201+09:00 INFO 6448 --- [ restartedMain] com.app.login.LoginApplication : No active profile set, falling back to 1 default profile: "default"
2023-09-21T16:16:19.312+09:00 INFO 6448 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-09-21T16:16:19.313+09:00 INFO 6448 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-09-21T16:16:22.075+09:00 INFO 6448 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-09-21T16:16:22.118+09:00 INFO 6448 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-09-21T16:16:22.123+09:00 INFO 6448 --- [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.12]
2023-09-21T16:16:22.312+09:00 INFO 6448 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-09-21T16:16:22.318+09:00 INFO 6448 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 3000 ms
LifecycleBean is initialized.
2023-09-21T16:16:23.425+09:00 INFO 6448 --- [ restartedMain] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@37a46c54, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@e52b243, org.springframework.security.web.context.SecurityContextHolderFilter@4bc2ec5, org.springframework.security.web.header.HeaderWriterFilter@4eb72c9e, org.springframework.web.filter.CorsFilter@e2e6051, org.springframework.security.web.authentication.logout.LogoutFilter@6455a2a4, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4429b5d4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@206dcbce, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@213502ab, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@73abefcc, org.springframework.security.web.access.ExceptionTranslationFilter@32a7c477, org.springframework.security.web.access.intercept.AuthorizationFilter@1ad34c99]
2023-09-21T16:16:23.811+09:00 INFO 6448 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is
running on port 35729
2023-09-21T16:16:23.909+09:00 INFO 6448 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-21T16:16:23.927+09:00 INFO 6448 --- [ restartedMain] com.app.login.LoginApplication : Started LoginApplication in 5.425 seconds (process running for 6.112)
+ LifecycleBean is about to be destroyed.