【Java】JakartaEE(JavaEE)でコンストラクタインジェクションがしたい
JakartaEE(JavaEE)でコンストラクタインジェクションがしたい
Spring BootのDIはよくお世話になってますが、JakartaEE(JavaEE)※以降JakartaEE を使う機会があったので、Springと同じようにDIができるか試してみます。
JakartaEEではCDI(Contexts and Dependency Injection)と呼ばれるみたいです。
インジェクトの種類
インジェクトするには下記の三種類があります。
フィールドに直接Inject
@RequestScoped
public class TestDomain {
@Inject
private TestRepository testRepository;
}
メソッドでInject
@RequestScoped
public class TestDomain {
private TestRepository testRepository;
@Inject
void setTestRepository(TestRepository testRepository) {
this.testRepository = testRepository;
}
}
コンストラクタでInject
@RequestScoped
public class TestDomain {
private final TestRepository testRepository;
@Inject
TestDomain(TestRepository testRepository) {
this.testRepository = testRepository;
}
}
今回はテストをしやすいように外から依存関係を注入できる後者2つ、
さらに依存関係を注入するフィールドを定数(final)にすべく、コンストラクタインジェクションでやりたいと思います。
CDIの準備
依存関係
weld-servlet-shadedを使います。
dependencies {
// Source: https://mvnrepository.com/artifact/org.jboss.weld.servlet/weld-servlet-shaded
implementation("org.jboss.weld.servlet:weld-servlet-shaded:7.0.0.Alpha1")
}
設定ファイル
本来はCDI1.1以上であれば不要なはずなんですが、beans.xmlのデフォルト設定である
bean-discovery-mode="annotated"
のままだと、どうもうまく動かなくて
bean-discovery-mode="all"
にするべくbeans.xmlを配置しました。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
bean-discovery-mode="all">
</beans>
bean-discovery-mode="annotated"だと、アノテーションがついているクラスしか認識させない設定なので、
パフォーマンスが向上して推奨されていますが、原因をいまだ調査中です…。
beans.xmlの配置場所
インジェクトされる.classファイルが、
・WEB-INF/lib配下にある場合
jarファイル内のMETA-INF/beans.xml
・WEB-INF/classes配下にある場合、
WEB-INF/beans.xml
に配置します。
インジェクションしてみる
フォルダ構成は下記のとおりです。
TestProject
┗WebContent
┗WEB-INF
┗lib
┗test.jar
┣META-INF
┃┣MANIFEST.MF
┃┗beans.xml
┗com
┗test
┣servlet
┃┗TestServlet.class
┣domain
┃┣TestRepository.class
┃┗TestService.class
┗infrastructure
┗TestDataSource.class
TestServlet.java
エンドポイントとなるサーブレットクラスです。
サーブレットはコンストラクタが作れないので、メソッドでインジェクトします。
package com.test.servlet;
import com.test.domain.TestService;
import jakarta.inject.Inject;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
private TestService testService;
@Inject
protected void setTestService(final TestService testService) {
this.testService = testService;
}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
try (PrintWriter out = response.getWriter()) {
out.write("{\"message\":\"" + Objects.requireNonNull(testService).findAll() + "\"}");
}
}
}
TestService.java
コンストラクタでインジェクトします。
引数に指定している型はinterfaceです。
package com.test.domain;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import java.util.Objects;
@RequestScoped
public class TestService {
private final TestRepository testRepository;
protected TestService() {
this.testRepository = null;
}
@Inject
protected TestService(final TestRepository testRepository) {
this.testRepository = testRepository;
}
public String findAll() {
return Objects.requireNonNull(testRepository).findAll();
}
}
TestRepository.java
interfaceです。
アノテーションは不要です。
package com.test.domain;
public interface TestRepository {
String findAll();
}
TestDataSource.java
interfaceの実装クラスです。
先ほどのTestServiceに注入されるクラスです。
package com.test.infrastructure;
import com.test.domain.TestRepository;
import jakarta.enterprise.context.RequestScoped;
@RequestScoped
public class TestDataSource implements TestRepository {
@Override
public String findAll() {
return "OK";
}
}
個人的感想
- フィールドインジェクション
- テストしにくい
- メソッドインジェクション
- 変数をfinalにできない
- コンストラクタインジェクション
- デフォルトコンストラクタが必要
- コンパイラでnull警告が出てしまう
なんでしょう…この何か足りない感じ…。
Lombokでコンストラクタ作成
Lombokでコンストラクタを作って、煩わしさを減らそうともしましたが…
@RequestScoped
@NoArgsConstructor(force = true, access = AccessLevel.PROTECTED)
@RequiredArgsConstructor(onConstructor_ = {@Inject}, access = AccessLevel.PROTECTED)
// こういう書き方もあるらしい?
// @RequiredArgsConstructor(onConstructor = @__({@Inject}), access = AccessLevel.PROTECTED)
public class TestDomain {
}
- 注入しているのがわかりにくい
- デフォルトコンストラクタだけLombok、引数付きコンストラクタだけ手動で作成すると、コンパイラがうまく認識してくれない
- Lombokの書き方が独特すぎる
- アノテーションを付与する方法(onConstructor)が、結局うまく動かなかった
- onConstructorじゃなければ、Lombokの設定が必要
ということで結局あきらめて、全部コンストラクタを書くようにしました。
まとめ
SpringのDIに慣れすぎて、CDIにはちょっと苦戦しました…。
なんというか、微妙にかゆいところに届かない感じですねぇ。
逆に改めてSpringがすごいということがわかりました。
以上、ここまで見ていただきありがとうございます。
皆さまの快適な開発ライフに、ほんの少しでもお役に立てれば幸いです。