25
21

More than 3 years have passed since last update.

1ヶ月SpringBootを触ってまだ理解できていないところ

Last updated at Posted at 2020-04-18

はじめに

Spring Bootを使ったアプリケーションの実装を行ってきて1ヶ月が経った。
しかしまだ良くわかってない部分が沢山あるので勉強したところをここにノート変わりとしてまとめていく:v:

DI(依存性の注入)とは?

IoC(Inversion of Cotnrol = 制御の反転)の一種。
IoCの中にはServiceLocatorというのもあるらしいがそれはこちらを参照して欲しい。

DIとは、Dependency Injectionの略で、Springの全ての土台となっている。
DIを簡単に説明するならば、インスタンスの管理である。

インスタンスの管理とは...?

  • DIコンテナの中で、クラスをnewしてインスタンスを生成する。
  • DIの中で毎回newしたインスタンスをアプリケーションに渡すのか、一度newしたインスタンスをアプリケーションに渡すのかを管理する。
  • サーブレットのリクエストスコープやセッションスコープにインスタンスを簡単に登録し実装できる。

依存性の注入とは...? DIは何をしてくれるの?

DIにおける「依存性」と「注入」の意味はこちらを参照。
「依存性の注入」と言う言葉が良くわからなければ、「オブジェクトの注入」と変えても良い。
DIは上の二つを同時に、簡単に行うが、他にも色々とある。:point_down:

DIの管理対象クラスを探す(コンポーネントスキャン)

Springを起動すると、コンポーネントスキャンという処理が走り、DIで管理する以下のような対象のアノテーションが付けられているクラスを探す。
@Component
@Controller
@Service
@Repository
@Configuration
@RestController
@ControllerAdvice
@ManagedBean
@Named

インスタンスの生成と注入

DIコンテナに登録されたBeanのインスタンス生成と注入をする。
DI対象のクラス(Bean)を集めた後は、それらのインスタンスをDIが生成(new)する。
そして、生成したインスタンスを以下の方法で注入(代入)する。

注入(インジェクション)方法

・フィールドインジェクション(非推奨になった)

フィールドインジェクションはフィールド(メンバ変数)に@Autowiredをつけるだけでインスタンスが注入(代入)される。

@Autowired
private SampleService sampleService;

しかし以下のようなデメリットがあるため、非推奨となった...

  1. テストコード作成時にDIコンテナを使わないとモックが出来ない
  2. フィールドにfinal属性を付与出来ずimmutableにならない

よって今では、以下のコンストラクタインジェクションを使う。

・コンストラクタインジェクション

@Service
@Transactional
public class SampleService {

  private final SampleRepository sampleRepository;

  // spring4.3以降はコンストラクタが1件の場合@Autowiredを省略できる
  @Autowired
  public SampleService(SampleRepository sampleRepository) {
    this.sampleRepository = sampleRepository;
  }

}

もっと簡単に書くには、Lombokを活用する。
@RequiredArgsConstructorをクラスにアノテートするだけ:point_down:

@Service
@Transactional
@RequiredArgsConstructor
public class SampleService {

  private final SampleRepository sampleRepository;

}

Beanとは?

DIコンテナ上で管理するクラスのことを『Bean』 と呼ぶ。
@Conponent@Controllerアノテーションがついたクラスや、@Beanと書いたメソッドでDIコンテナにクラスを登録したものを指す。

DIの実装方法

JavaConfigのDI実装方法

まず、DIコンテナに登録するBeanを定義する。

JavaConfig.java
@Configuration
public class JavaConfig {
  @Bean
  public SampleComponent sampleComponent {
    return new SampleComponent();
  }
  @Baan
  public SampleController sampleService {
    return new SampleService();
  }
}

これで、@Beanを付けたgetterの戻り値が、DIコンテナのBeanとして登録される。
JavaConfigではDIコンテナで管理するインスタンス分のメソッドを用意しなければならない。
しかし、インスタンスを生成する際にコンストラクタなどに渡す値などを設定できたり、本番環境と開発環境のJavaConfigクラスを切り替えたりできる。

アノテーションベースでDI実装

アノテーションベースでは、@Component@Controllerなどがついたクラスを、Springが自動的にDIコンテナに登録してくれる。

DIのライフサイクル管理とは...?

ライフサイクル管理とは、インスタンスの生成(new)と破棄の管理である。
Webアプリケーションの作成でサーブレットを使う場合、インスタンスをSessionスコープやRequestスコープに登録するが、そのインスタンスがいつ破棄されるかしっかりと把握する必要性がある。

スコープとは...?

SessionスコープやRequestスコープと言うのは、インスタンスの有効期限である。2つのスコープの有効期限は以下のように異なる。

Sessionスコープ

ユーザーがログインしてからログアウトするまでが有効期限。例えば、ユーザーがログインしているかどうかなどの情報をSessionスコープとして持っておける。
以下のコードは自分がOAuthを使ったアプリケーションを作った時に使用したコードを引っ張ってきた。

OAuthDTO.java
@Data
@Component
@SessionScope
public class OAuthDTO {
  private boolean isLogin;
}

Requestスコープ

HTTPの1リクエストが有効期限。例えば、ユーザーログイン画面から、ログインボタンを押してユーザープロフィール画面に遷移するまでがRequestスコープの範囲となる。
自分はコントローラが呼ばれる前に処理されるインタセプターで使用した。

OAuthInterceptor.java
@Component
@RequiredArgsConstructor
@RequestScope
public class OAuthInterceptor extends HandlerInterceptorAdapter {
  private final OAuthDTO oAuthDTO;

  /** {@inheritDoc} */
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws AuthenticationException {
    if (!oAuthDTO.isLogin()) {
      throw new AuthenticationException("ログインしてください。");
    }
    return true;
  }
}

そのほかのスコープ

・singleton
Spring起動時にインスタンスを1つだけ生成する。生成後は、1つのインスタンスを共有して使う。
デフォルト設定のため、@Scopeアノテーションを付けない場合は全てsingletonとなる。

・prototype
Beanを取得するたびに、毎回インスタンスが生成される

・globalSession
ポートレット環境におけるGlobalSession単位でインスタンスが生成される。
ポートレットに対応したWebアプリケーションの場合のみ使用できる。

・application
サーブレットのコンテキスト単位でインスタンスが生成される。

以上のように、@Scopeアノテーションを使用することで、簡単にインスタンスの生成と破棄ができる。
@Component以外にも、@Bean@Controllerなどにも@Scopeアノテーションは利用可能

DIで気をつけなければならないこと

singleton

@Scopeアノテーションを付けないとインスタンスはsingletonで作成される。つまり、オブジェクトのインスタンスは1つしか作られない。
スコープは意識しておかないとバグの発生原因になりかねない。
@Controller@Service@Repositoryのスコープは通常singletonで十分

スコープの違い

例えば、singletonスコープのインスタンスが、sessionスコープオブジェクトを持っている場合。

OAuthDTO.java
@Data
@Component
@SessionScope
public class OAuthDTO {
  private boolean isLogin;
}
OAuthService.java
@Service
@RequiredArgsConstructor
public class OAuthService {

   private final OAuthDTO oAuthDTO;

}

このようなことになってしまうと、sessionスコープを設定したBean(OAuthDTO.java)がsingletonスコープに変化してしまう。

4月28日追記
申し訳ありません。こちらの部分間違っていました:bow:

@SessionScope はSpring4.3から使用できるようになり、元は
@Scope(value = “session”, proxyMode = ScopedProxyMode.TARGET_CLASS)
です。
で、proxyMode = ScopedProxyMode.TARGET_CLASSScoped Proxyと呼ばれ、これはProxyで包んだ状態でBeanをインジェクションし、インジェクションされたBeanのメソッドを呼ぶと、実際はDIコンテナからルックアップしたBeanにメソッドを委譲するものです。

つまり...
スコープの変化は発生しない。現状 scoped-proxy によって、特に controller や interceptor では気にせず自身より狭いスコープのものも DI コンテナから受け取る実装になっていても問題ない作りになっている。

prototype等に関しては以下を参考にするのが良い。

しかし、上の例のコードが間違っていない訳ではない。
OAuthDTO.java を OAuthService.java にインジェクトすることによってServiceクラスが状態を持つものになっている。
ServiceクラスはControllerクラスからのみ呼び出されるわけではないため、スコープ以前に、HTTPリクエストなど特定のコンテキストに縛られた状態を持っていると、それが存在しない経路で実行される時に、どこからそのインスタンスを調達するか問題になる。

よって、上の例のコードではOAuthDTO.java は Controllerクラスにインジェクトするのが良い。

実際の実装法は以下を参考にすると良い。

ここからは良く参考にするサイトまとめ

アノテーション

Lombok

コードレビューポイント

25
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
21