外部のAPIに接続する際に、Basic認証を使用するシーンはまだよく見かける。
このときのクレデンシャル(ユーザーIDとパスワード)をどこに保存するのか問題。
Javaでは、環境変数はapplication.env.ymlに記述するのが一般的だが、これだとGitで見れてしまう。
そこで、サーバー側で変数として持っておいて、アプリケーションから参照したい。
JNDIで実現できる
Javaでは、JNDIという仕組みを使用する。
JNDIは、Java Naming and Directory Interfaceの略称であり、この名前からもじんわり伝わってくる通り、サーバー内にネームスペースを作ってアプリケーション側から参照できるようにしようぜ、の仕組みである。
もっと簡単に言えば、サーバーで定義した変数をアプリから使えるようにしようぜ、と、タイトルの通りとなる。
ちなみにJNDIは、よくDBの接続情報を定義するのに使用されている。
実装方法
実装方法は以下の通り。
- Tomcatのcontext.xmlに変数を定義する
- アプリケーション側で呼び出す
Tomcatのcontext.xmlに変数を定義する
修正ファイル:/opt/tomcat/conf/context.xml
※/opt/tomcat/conf/Catalina/localhost/[appName].xml でも可
<Context>
<Environment
type="java.lang.String"
name="varName"
value="sampleVal"/>
</Context>
アプリケーション側で呼び出す
修正ファイル:任意のJavaファイル
Context ctx = new InitialContext();
String varName = (String) ctx.lookup("java:comp/env/varName");
java:comp/envが難所。
ネームスペースの命名はJNDIで決まっており、単純に変数名だけではなく、java:comp/env:変数名 として参照しなければならない。
※ネームスペースはいろいろあるが、たぶん他は特殊なことしない限りアプリから呼び出したりしないので、今回の設定だけわかっていればよい
context.xmlのテスト
contextは、明確にリセットしないと、1つのテストでの設定が他のテストでも共有されてしまう。
さらに、context を再設定するためにNamingManagerにContextBuildeを編集しようとすると以下のエラーが発生する。
java.lang.IllegalStateException: InitialContextFactoryBuilder already set
そのため、LocalThreadで起動して、 テストのたびにThreadを破棄するといった構成にする必要がある。
設定の複雑さもさることながら、ソースコードはより複雑怪奇なものとなる。
【前提】
- サーバーのcontext.xmlに、ResourceとしてcontextParamが設定されている(値はsampleVal)
- 検証メソッド:public Response SampleService.testMethod() {}
- Responseのパラメータに、context.xmlから取得した、contextParamを設定している
- contextが取得できなかった場合は、NamingExceptionが発生する
■検証するクラス
@Service
public class SampleService {
private static final Logger LOGGER = LoggerFactory.getLogger(SampleService.class);
public Response testMethod() {
Response response = new Response();
String contextParam = "";
try {
Context ctx new InitialContext();
contextParam (String) ctx.lookup("java:comp/env/contextParam");
} catch (NamingException e) {
LOGGER.debug(e1.getMessage());
throw e;
}
response.setContextParam(contextParam);
return response;
}
}
■テストクラスの実装
public class SapmleTest {
private static final ThreadLocal threadLocalContext = new ThreadLocal<>();
private static String contextParam = "sampleVal";
@Inject Mocks
static private SapmleService service;
@BeforeAll
public static void setUpBeforeClass() throws NamingException {
InitialContextFactory factory = env -> threadLocalContext.get();
setInitialContextFactoryBuilder factoryBuilder = env -> factory;
NamingManager.setInitialContextFactoryBuilder(factoryBuilder);
}
@BeforeEach
public void setUp() throws NamingException {
// 各テスト開始時にスレッドにコンテキストを設定
InitialContext mockCtx = mock(InitialContext.class);
threadLocalContext.set(mockCtx);
}
@AfterEach
void tearDown() throws Exception {
// 各テスト終了時にはスレッドをリセット
threadLocalContext.remove();
}
@DisplayName("コンテキスト取得テスト"")
@Test
void testMethod() throws NamingException {
// コンテキスト設定
InitialContext mockCtx = new Initial Context();
when(mockCtx.lookup("java:comp/env/contextParam")).thenReturn(contextParam);
// 実行
Response actualResponse = service.testMethod();
// 期待:コンテキストから首都kした値がレスポンスに設定されている
assertThat(actualResponse.getContextParam()).isEqualTo("sampleVal");
}
@DisplayName ("コンテキストを取得できなかった場合")
@Test
void test Cant Get Credential() {
threadLocalContext.remove();
// 明示的にスレッドをリセット(AfterEachでもリセットされるはずだけど念のため)
// スレッドがリセットされていないと、コンテキストが残ったままのため、例外が発生しない
// 実行
assertThatThrownBy (() -> service.testMethod()).isInstance0f(NamingManagerException.class);
}
}
Appendix
web.xmlでアプリケーションから参照できるようにする
context.xmlが何かの事情で修正できない場合は、web.xmlでも変数定義が可能。
修正ファイル:/opt/tomcat/webapps/appName/WEB-INF/web.xml
<web-app>
<env-entry>
<env-entry-name>varName</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value>sampleVal</env-entry-value>
</env-entry>
</web-app>
コメント