/*
* Copyright (c) 2016 NTT DATA Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.terasoluna.fw.batch.executor;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import jp.terasoluna.fw.batch.constants.LogId;
import jp.terasoluna.fw.batch.executor.vo.BatchJobData;
import jp.terasoluna.fw.logger.TLogger;
/**
* DIコンテナのキャッシュを実現する{@code ApplicationContextResolver}実装。
* 非同期バッチ起動を行い同じジョブを繰り返し実行する場合、DIコンテナのキャッシュによる性能向上が見込まれる。
* <p>
* 本クラスではSpring Cache Abstractionを用いて、ジョブ業務コードをキーとしたDIコンテナのキャッシュを行う。
* キャッシュの対象となるのはジョブBean定義ファイルにもとづいたDIコンテナのみであり、システム用アプリケーションコンテキストは対象としない。
*
* DIコンテナのキャッシュを使用するためには、Bean定義ファイル内に{@code CacheManager}の定義・インジェクションが必要となる。
* </p>
* <p>
* Bean定義ファイルの記述例:
* <code><pre>
* <!-- cache名前空間のXMLスキーマ定義を追加 -->
* <beans xmlns="http://www.springframework.org/schema/beans"
* xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
* xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
* (略)
*
* <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
* <property name="caches">
* <set>
* <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
* <!-- DIコンテナのキャッシュ名はbusinessContext固定 -->
* <property name="name" value="businessContext"/>
* </bean>
* </set>
* </property>
* </bean>
* <bean id="blogicContextResolver" class="jp.terasoluna.fw.batch.executor.CacheableApplicationContextResolverImpl">
* <!-- 共通コンテキストをDIコンテナの親とする場合、commonContextClassPathでBean定義ファイルのクラスパスを記述する。(複数指定時はカンマ区切り) -->
* <property name="commonContextClassPath" value="beansDef/commonContext.xml,beansDef/dataSource.xml"/>
* <!-- cacheManagerのsetter-injection -->
* <property name="cacheManager" ref="cacheManager"/>
* </bean>
* (略)
* </beans>
* </pre></code>
* </p>
* <p>
* 使用上の注意点として、上記記述例で使用しているキャッシュ名のbusinessContextは固定名であり、
* 変更することはできない。
* 既にSpring Cache Abstractionの{@code ConcurrentMapCacheFactoryBean}による
* ローカルキャッシュを使用している場合、{@code cacheManager}のBean定義にbusinessContextのキャッシュ領域を追加し、
* {@code cacheManager}をインジェクションすることで本機能によるDIコンテナのキャッシュと併用可能となる。
*
* また、{@code closeApplicationContext()}メソッドではキャッシュ対象のDIコンテナのクローズは行わず、
* {@code #destroy()}メソッドで一括でクローズする。
* </p>
* @since 3.6
*/
public class CacheableApplicationContextResolverImpl
extends ApplicationContextResolverImpl
implements InitializingBean, DisposableBean {
/**
* ロガー
*/
private static final TLogger LOGGER = TLogger.getLogger(
CacheableApplicationContextResolverImpl.class);
/**
* ジョブ業務コードごとのロックモニタを格納するホルダー
*/
private ConcurrentMap<String, Object> lockMonitorHolder = new ConcurrentHashMap<>();
/**
* DIコンテナキャッシュを管理するキャッシュマネージャー
*/
protected CacheManager cacheManager;
/**
* キャッシュ対象となるDIコンテナのキャッシュキー
*/
public static final String BLOGIC_CONTEXT_CACHE_KEY = "businessContext";
/**
* DIコンテナのキャッシュを保持するキャッシュマネージャを設定する。
*
* @param cacheManager キャッシュマネージャ
*/
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
/**
* {@inheritDoc}
* <p>
* ジョブ業務コードをキーとして、キャッシュ済みのDIコンテナを返却する。<br>
* キャッシュが行われていない場合、親クラスによってDIコンテナを生成し、結果をキャッシュする。
* </p>
* @param batchJobData ジョブ実行時のパラメータ(ジョブ業務コード{@code BatchJobData.jobAppCd}がキャッシュキーとなる)
*/
@Override
public ApplicationContext resolveApplicationContext(
BatchJobData batchJobData) {
Cache cache = this.cacheManager.getCache(BLOGIC_CONTEXT_CACHE_KEY);
String jobAppCd = batchJobData.getJobAppCd();
// すでにキャッシュされていれば、それを返却する
ApplicationContext jobAppCtx = cache.get(jobAppCd, ApplicationContext.class);
if (jobAppCtx != null) {
return jobAppCtx;
}
// まだキャッシュされていない場合、ジョブ業務コードごとに同期化して、コンテキストを生成しキャッシュする
Object lockMonitor = getLockMonitor(jobAppCd);
synchronized (lockMonitor) {
jobAppCtx = cache.get(jobAppCd, ApplicationContext.class);
if (jobAppCtx == null) {
LOGGER.info(LogId.IAL025019, batchJobData.getJobAppCd());
jobAppCtx = super.resolveApplicationContext(batchJobData);
cache.put(jobAppCd, jobAppCtx);
}
}
return jobAppCtx;
}
/**
* ジョブ業務コードに対応するロックモニタを返却する。
*
* @param key ジョブ業務コード
* @return ロックモニタオブジェクト
*/
private Object getLockMonitor(String key){
Object lockObjCandidate = new Object();
Object lockObj = lockMonitorHolder.putIfAbsent(key, lockObjCandidate);
// まだホルダーになかった場合はnullが返ってくるので、モニタ候補を正式版に格上げする
// ホルダーにあった場合はそれを使う
if (lockObj == null) {
lockObj = lockObjCandidate;
}
return lockObj;
}
/**
* {@inheritDoc}
*
* キャッシュ機能利用が前提となるため、本メソッドはスキップする。
*
* @param applicationContext 業務用Bean定義のアプリケーションコンテキスト
*/
@Override
public void closeApplicationContext(ApplicationContext applicationContext) {
// キャッシュされたDIコンテナをクローズしない。
}
/**
* 初期化処理としてキャッシュ機能が使用不可能な状態であるとき、{@code BeanCreationException}をスローする。
*/
@Override
public void afterPropertiesSet() {
if (!isCacheEnabled()) {
// キャッシュ機能使用不可の場合
throw new BeanCreationException(LOGGER.getLogMessage(LogId.EAL025061, BLOGIC_CONTEXT_CACHE_KEY));
}
super.afterPropertiesSet();
}
/**
* 本インスタンス破棄時、共有コンテキスト及びキャッシュとして保持されている
* DIコンテナの破棄を行う。
*/
@Override
public void destroy() {
destroyCachedContext();
// 子コンテキストを破棄しても親コンテキストは破棄されないため、
// DIコンテナ破棄の後で親である共通コンテキストの破棄を行う。
super.destroy();
}
/**
* キャッシュされたDIコンテナの破棄とキャッシュ自身の破棄を行う。
*/
protected void destroyCachedContext() {
Cache cache = this.cacheManager.getCache(BLOGIC_CONTEXT_CACHE_KEY);
Collection<?> cacheValues = Map.class.cast(cache.getNativeCache())
.values();
for (Object obj : cacheValues) {
if (obj instanceof ApplicationContext) {
super.closeApplicationContext(ApplicationContext.class.cast(obj));
}
}
cache.clear();
}
/**
* DIコンテナがキャッシュ可能であるかを判定する。
*
* @return キャッシュ可能ならばtrue、キャッシュ機能を使用していないためキャッシュ不可能ならばfalse
*/
protected boolean isCacheEnabled() {
if (this.cacheManager == null || !this.cacheManager.getCacheNames()
.contains(BLOGIC_CONTEXT_CACHE_KEY)) {
return false;
}
Cache cache = this.cacheManager.getCache(BLOGIC_CONTEXT_CACHE_KEY);
if (cache == null) {
return false;
}
// NoOpCache使用時以外はConcurrentMapCacheとなる。
return cache.getNativeCache() instanceof Map;
}
}