[Java Spring MVC]自作クラスでDIを使いたい

2018-11-05 追記

* 以下に書いたDI解決方法（WebApplicationContext経由でBeanを取り出す方法）だと、バッチやJunitテストで実行した際にコンテキストがないよというエラーになるため、スタッフで話し合い、今回のプロジェクトでは使わないように取り決められた。
* 自作したオブジェクト内でDI解決が必要な場合、その自作クラスのオブジェクトはControllerやServiceといった@Inject(@Autowired)が利用できるクラス内でインスタンス化する。
* Dtoなどデータ保持オブジェクトにこの自作オブジェクトの機能をもたせたい場合、DI経由でインスタンス化したオブジェクトをDtoオブジェクトなどにセットするか、処理結果の値そのものをセットするようにする。
* Dtoクラスなど、通常はインスタンス化する際に@Inject（@Autowired）経由ではなく単純にnewが使われるようなクラス内ではDI解決の必要な自作クラスのオブジェクトを生成させない。ControllerなどDI解決が可能なクラスからの注入を受ける立場とする。

PHPのLaravelではどんなクラスからでもグローバルにDI解決経由でオブジェクトを取得しても問題が起きなかった（私の知ってる範囲内では起きなかった）が、Springではコンテキストというめんどくさい要素が絡むため、LaravelのようなノリでホイホイとDI解決をやらせることはできないらしい。

これも例によってSpringBootが使われてない状態での話なので、SpringBootが導入されるとそういうわけでもなくなるのかもしれないが、現段階ではそのへんの調査はできていない。

---


Java の Spring MVC を使ったWebシステムの案件にアサインされた。
ちょっと昔に作られたシステムらしく、Spring Boot は利用されていない。
私自身がJavaでのWebシステムにまだ不慣れなのでとてもやりにくい。

Spring MVC では「@Inject」みたいなアノテーションを使ってDIを行っているようだ。
最近ドメイン駆動開発について少し知ったので、いっちょドメインオブジェクト（わかりやすい仕事の単位を担うオブジェクト）でも作ってやるかということで改修部分に関わる簡単なドメインオブジェクトを作ろうとした。

ところが知ったのは、Spring MVCのルールに則っているわけではないクラス、つまり自作クラス内ではアノテーションを使ったDIが使えない。
代替手段を模索した結果それなりにわかったので軽くここにまとめる。

## 概要
* アノテーションを使って解決されるDIと同じ文脈に乗っかったDI解決をどこからでも呼び出せるようにしたい。

```java
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.WebApplicationContextUtils;


(中略)

    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
    HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
    ServletContext servletContext = httpServletRequest.getServletContext();
    WebApplicationContext webApplicationContext =
        WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);

    // Bean定義名でインスタンス取得
    // Object型で返るので目当てのクラスにキャストする
    目当ての型 変数名 = (目当ての型にキャスト) webApplicationContext.getBean("Bean定義名");

    // Bean定義名一覧を取得 getBean() の引数にできる文字列を調べられる。
    String[] beanDefinitionNames = webApplicationContext.getBeanDefinitionNames();
```


## ApplicationContext.getBean("Bean定義名")
* アノテーションを使わないDI解決でのインスタンス取得。
* Object型で返ってくるので受け取り側で目当てのクラス（インターフェイス）にキャストする。
* ApplicationContextインスタンスは `WebApplicationContextUtils.getRequiredWebApplicationContext(ServletContextインスタンス)` で、「WebApplicationContext」が取得できる。
* ではServletContextはどうやって入手するか

## ServletContext の入手
* ServletContext はHttpServletRequestインスタンスから `getServletContext()` で取得できる。
* HttpServletRequestインスタンスは `((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()` で入手できる。

## Bean定義名はどうやって調べるか
* `ApplicationContext.getBean()` の引数として利用できるBean定義名は、`WebApplicationContext.getBeanDefinitionNames()` で取得できる。
* 標準出力するなりログ出力するなりして確認する。

## サンプルクラス
* 自作クラスのためにDI解決を行うクラスを作ってみた。
* Spring Boot については未学習なのでBootに乗っかればこういう工夫は不要になるのかもしれない。

```java
package パッケージ名を指定;

import java.util.Arrays;
import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * ドメインオブジェクト
 * DI（DependencyInjectionSolver）ソルバー
 * Spring管理外の自作クラスでは@Injectなどのアノテーションを使ったDIが効かないので、
 * このクラスを経由してDI解決されたインスタンスを取得する。
 *
 * このクラスを利用する処理をテストする際、JUnitのTestクラスに「@WebAppConfiguration」のアノテーションを付けること。
 */
public class DependencyInjectionSolver {
  private static final Logger logger = LoggerFactory.getLogger(DependencyInjectionSolver.class);

  /**
   * このインスタンスを通してBeanを取得するためのApplicationContext
   */
  private WebApplicationContext webApplicationContext;

  public DependencyInjectionSolver() {
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
    HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
    ServletContext servletContext = httpServletRequest.getServletContext();
    this.webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
  }

  /**
   * Bean定義名を受け取ってDI解決されたインスタンスを取得する。
   * Object型で返るので取得側でキャストすること。
   *
   * @param  string Bean定義名
   * @return Object
   */
  public Object getBeanByBeanName(String beanName) {
    return this.webApplicationContext.getBean(beanName);
  }

  /**
   * 定義済みのBean定義名一覧を取得する。
   * このリストに含まれる名称を getBeanByBeanName() の引数として利用することができる。
   *
   * @return List<string> Bean定義名の一覧
   */
  public List<String> gettableBeanNames() {
    String[] beanDefinitionNames = this.webApplicationContext.getBeanDefinitionNames();
    return Arrays.asList(beanDefinitionNames);
  }
}
```

利用側

```java
DependencyInjectionSolver diSolver = new DependencyInjectionSolver();
インターフェイス名など 変数名 = (インターフェイス名など) diSolver.getBeanByBeanName("Bean定義名");
```