init
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
package com.eactive.eai.common.ibatis;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.orm.ibatis.SqlMapClientFactoryBean;
|
||||
|
||||
import com.ibatis.sqlmap.client.SqlMapClient;
|
||||
import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
|
||||
|
||||
/**
|
||||
* iBATIS sqlmap 클라이언트의 sqlMap 및 sqlMapConfig 파일의 변경을 감지, 실시간 적용하는 팩토리 빈.
|
||||
*
|
||||
* <h2>개요</h2>
|
||||
* iBATIS + Spring 개발시 쿼리 매핑 파일이 변경되면 웹애플리케이션 서버를 재기동해야 적용이 됐었다. 이러한 불편을 없애기
|
||||
* 위해 매핑 파일 변경을 실시간으로 감시, 적용하는 모듈을 제공한다.<br />
|
||||
*
|
||||
* 감시 대상 이 모듈은 iBATIS sqlmap 클라이언트의 sqlMap 및 sqlMapConfig 파일의 변경을 감지, 실시간 적용해준다.<br />
|
||||
*
|
||||
* <h2>제약사항</h2>
|
||||
* 감시 대상 파일들은 스타트업 당시에 결정된다. 그러므로 추가된 파일들에 대해서는 감지가 되지 않고, 삭제된 파일들에 대해서는 경고
|
||||
* 메시지가 나온다. 예를 들어, sqlMapConfig에 sqlMap 파일이 추가되거나 하면 해당 맵이 적용되기는 하지만, 실시간 변경 감지
|
||||
* 대상으로 추가되지는 않는다.<br />
|
||||
*
|
||||
* <h2>요구사항</h2>
|
||||
* iBATIS sqlmap 2.3.0, Java 1.4, Spring 2.5 이상 또는 iBATIS sqlmap 2.3.2 이상,<br />
|
||||
* Java 1.5 이상, Spring 2.5.5 이상<br />
|
||||
*
|
||||
* <h2>적용 순서</h2>
|
||||
* 1. Spring의 applicationContext 설정 파일 중 sqlMapClient를 얻기 위한
|
||||
* SqlMapClientFactory 빈을 신규 클래스로 교체한다. <br />
|
||||
* 2. 변경 감지 시간 간격 (1000분의 1초 단위)를 지정한다.<br />
|
||||
* <PRE>
|
||||
* <bean id="sqlMapClient" class="jcf.dao.ibatis.sqlmap.RefreshableSqlMapClientFactoryBean">
|
||||
* <!-- <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">-->
|
||||
* <property name="configLocation" value="classpath:jcf/inboundErrorInfoDao/ibatis/sqlmap/sqlmap-config.xml" />
|
||||
* <property name="dataSource" ref="dataSource" />
|
||||
*
|
||||
* <!-- Java 1.5 or higher and iBATIS 2.3.2 or higher REQUIRED -->
|
||||
* <property name="mappingLocations" value="jcf/inboundErrorInfoDao/\*\*\/T\*.xml" />
|
||||
* <!-- <property name="mappingLocations" value="file:///D:/Type.xml" />-->
|
||||
*
|
||||
* <property name="checkInterval" value="1000" />
|
||||
* </bean>
|
||||
* </PRE>
|
||||
*
|
||||
* 3. 라이브러리 추가 <br />
|
||||
* backport-util-concurrent-3.1.jar<br />
|
||||
*
|
||||
* 테스트 기존의 applicationContext에 등록되어 있던 sqlMapClient에 해당하는 mappingFile들을 편집하거나,
|
||||
* sqlMapConfig 파일 또는 그 파일에 등록된 mappingFile(sqlMap 파일)들을 편집하면 주어진 변경감지 시간 후 변경사항이 적용된다.<br />
|
||||
* <br />
|
||||
*
|
||||
* @author setq
|
||||
*/
|
||||
public class RefreshableSqlMapClientFactoryBean extends SqlMapClientFactoryBean
|
||||
implements SqlMapClientRefreshable, DisposableBean {
|
||||
|
||||
private static final Log logger = LogFactory
|
||||
.getLog(RefreshableSqlMapClientFactoryBean.class);
|
||||
|
||||
private SqlMapClient proxy;
|
||||
private int interval;
|
||||
|
||||
private Timer timer;
|
||||
private TimerTask task;
|
||||
|
||||
private Resource[] configLocations;
|
||||
|
||||
private Resource[] mappingLocations;
|
||||
|
||||
/**
|
||||
* 파일 감시 쓰레드가 실행중인지 여부.
|
||||
*/
|
||||
private boolean running = false;
|
||||
|
||||
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
|
||||
private final Lock r = rwl.readLock();
|
||||
private final Lock w = rwl.writeLock();
|
||||
|
||||
public void setConfigLocation(Resource configLocation) {
|
||||
super.setConfigLocation(configLocation);
|
||||
this.configLocations = (configLocation != null ? new Resource[] { configLocation }
|
||||
: null);
|
||||
}
|
||||
|
||||
public void setConfigLocations(Resource[] configLocations) {
|
||||
super.setConfigLocations(configLocations);
|
||||
this.configLocations = configLocations;
|
||||
}
|
||||
|
||||
public void setMappingLocations(Resource[] mappingLocations) {
|
||||
super.setMappingLocations(mappingLocations);
|
||||
this.mappingLocations = mappingLocations;
|
||||
}
|
||||
|
||||
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
|
||||
public void doReload() throws Exception {
|
||||
if(executor.getActiveCount() == executor.getMaximumPoolSize()){
|
||||
return;
|
||||
}
|
||||
Runnable run = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
refresh();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while reloading sqlMapClient", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
executor.execute(run);
|
||||
}
|
||||
/**
|
||||
* iBATIS 설정을 다시 읽어들인다.<br /> SqlMapClient 인스턴스 자체를 새로 생성하여 교체한다.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public void refresh() throws Exception {
|
||||
/*
|
||||
* WRITE LOCK.
|
||||
*/
|
||||
w.lock();
|
||||
try {
|
||||
super.afterPropertiesSet();
|
||||
logger.info("Refreshed SqlMapClient.");
|
||||
} finally {
|
||||
w.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 싱글톤 멤버로 SqlMapClient 원본 대신 프록시로 설정하도록 오버라이드.
|
||||
*/
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
setRefreshable();
|
||||
}
|
||||
|
||||
private void setRefreshable() {
|
||||
proxy = (SqlMapClient) Proxy.newProxyInstance(SqlMapClient.class
|
||||
.getClassLoader(), new Class[] { SqlMapClient.class,
|
||||
ExtendedSqlMapClient.class }, new InvocationHandler() {
|
||||
|
||||
public Object invoke(Object proxy, Method method, Object[] args)
|
||||
throws Throwable {
|
||||
|
||||
return method.invoke(getParentObject(), args);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
task = new TimerTask() {
|
||||
|
||||
private Map map = new HashMap();
|
||||
|
||||
public void run() {
|
||||
|
||||
if (isModified()) {
|
||||
try {
|
||||
logger.info("Refreshing SqlMapClient...");
|
||||
// refresh();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
doReload();
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("caught exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isModified() {
|
||||
boolean retVal = false;
|
||||
|
||||
for (int i = 0; i < configLocations.length; i++) {
|
||||
Resource configLocation = configLocations[i];
|
||||
retVal |= findModifiedResource(configLocation);
|
||||
}
|
||||
|
||||
if (mappingLocations != null) {
|
||||
for (int i = 0; i < mappingLocations.length; i++) {
|
||||
Resource mappingLocation = mappingLocations[i];
|
||||
retVal |= findModifiedResource(mappingLocation);
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private boolean findModifiedResource(Resource resource) {
|
||||
boolean retVal = false;
|
||||
List modifiedResources = new ArrayList();
|
||||
|
||||
try {
|
||||
long modified = resource.lastModified();
|
||||
|
||||
if (map.containsKey(resource)) {
|
||||
long lastModified = ((Long) map.get(resource)).longValue();
|
||||
|
||||
if (lastModified != modified) {
|
||||
map.put(resource, new Long(modified));
|
||||
modifiedResources.add(resource.getDescription());
|
||||
retVal = true;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
map.put(resource, new Long(modified));
|
||||
}
|
||||
}
|
||||
catch (Throwable e) {
|
||||
logger.error("caught exception", e);
|
||||
}
|
||||
|
||||
if (retVal) {
|
||||
// if (logger.isInfoEnabled()) {
|
||||
// logger.info("modified files : " + modifiedResources);
|
||||
// }
|
||||
logger.info("modified files : " + modifiedResources);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
timer = new Timer(true);
|
||||
resetInterval();
|
||||
|
||||
|
||||
List mappingLocationList = extractMappingLocations(configLocations);
|
||||
|
||||
if (this.mappingLocations != null) {
|
||||
mappingLocationList.addAll(Arrays.asList(this.mappingLocations));
|
||||
}
|
||||
this.mappingLocations = (Resource[]) mappingLocationList.toArray(new Resource[0]);
|
||||
}
|
||||
|
||||
private List extractMappingLocations(Resource[] configLocations) {
|
||||
List mappingLocationList = new ArrayList();
|
||||
SqlMapExtractingSqlMapConfigParser configParser = new SqlMapExtractingSqlMapConfigParser();
|
||||
for (int i = 0; i < configLocations.length; i++) {
|
||||
try {
|
||||
InputStream is = configLocations[i].getInputStream();
|
||||
mappingLocationList.addAll(configParser.parse(is));
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.warn("Failed to parse config resource: "
|
||||
+ configLocations[i], ex.getCause());
|
||||
}
|
||||
}
|
||||
return mappingLocationList;
|
||||
}
|
||||
|
||||
private Object getParentObject() {
|
||||
/*
|
||||
* READ LOCK.
|
||||
*/
|
||||
r.lock();
|
||||
try {
|
||||
return super.getObject();
|
||||
|
||||
} finally {
|
||||
r.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlMapClient getObject() {
|
||||
return this.proxy;
|
||||
}
|
||||
|
||||
public Class getObjectType() {
|
||||
return (this.proxy != null ? this.proxy.getClass() : SqlMapClient.class);
|
||||
}
|
||||
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setCheckInterval(int ms) {
|
||||
interval = ms;
|
||||
|
||||
if (timer != null) {
|
||||
resetInterval();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetInterval() {
|
||||
if (running) {
|
||||
timer.cancel();
|
||||
running = false;
|
||||
}
|
||||
if (interval > 0) {
|
||||
timer.schedule(task, 0, interval);
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() throws Exception {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.eactive.eai.common.ibatis;
|
||||
|
||||
|
||||
public interface SqlMapClientRefreshable {
|
||||
|
||||
void refresh() throws Exception;
|
||||
|
||||
|
||||
void setCheckInterval(int ms);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.eactive.eai.common.ibatis;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import com.ibatis.common.xml.Nodelet;
|
||||
import com.ibatis.common.xml.NodeletParser;
|
||||
import com.ibatis.common.xml.NodeletUtils;
|
||||
import com.ibatis.sqlmap.client.SqlMapException;
|
||||
import com.ibatis.sqlmap.engine.builder.xml.SqlMapClasspathEntityResolver;
|
||||
//import com.ibatis.sqlmap.engine.builder.xml.XmlParserState;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class SqlMapExtractingSqlMapConfigParser {
|
||||
|
||||
protected final NodeletParser parser = new NodeletParser();
|
||||
// private XmlParserState state = new XmlParserState();
|
||||
|
||||
private List sqlMapList = new ArrayList();
|
||||
|
||||
private ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
public SqlMapExtractingSqlMapConfigParser() {
|
||||
parser.setValidation(true);
|
||||
parser.setEntityResolver(new SqlMapClasspathEntityResolver());
|
||||
|
||||
addSqlMapNodelets();
|
||||
|
||||
}
|
||||
|
||||
public List parse(Reader reader) {
|
||||
try {
|
||||
parser.parse(reader);
|
||||
return sqlMapList;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Error occurred. Cause: " + e, e);
|
||||
}
|
||||
}
|
||||
|
||||
public List parse(InputStream inputStream) {
|
||||
try {
|
||||
parser.parse(inputStream);
|
||||
return sqlMapList;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Error occurred. Cause: " + e, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addSqlMapNodelets() {
|
||||
parser.addNodelet("/sqlMapConfig/sqlMap", new Nodelet() {
|
||||
public void process(Node node) throws Exception {
|
||||
// state.getConfig().getErrorContext().setActivity(
|
||||
// "loading the SQL Map resource");
|
||||
|
||||
Properties attributes = NodeletUtils.parseAttributes(node,
|
||||
// state.getGlobalProps());
|
||||
null);
|
||||
|
||||
String resource = attributes.getProperty("resource");
|
||||
String url = attributes.getProperty("url");
|
||||
|
||||
if (resource != null) {
|
||||
sqlMapList.add(resourceLoader.getResource(resource));
|
||||
}
|
||||
else if (url != null) {
|
||||
sqlMapList.add(resourceLoader.getResource(url));
|
||||
}
|
||||
else {
|
||||
throw new SqlMapException(
|
||||
"The <sqlMap> element requires either a resource or a url attribute.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user