요즘 ORM으로는 하이버네이트, JPA등 많이 사용하고 있으나, 역시 SI 쪽은 mybatis(ibatis)를 많이 사용된다.
문제는 mybatis는 xml로 sql을 관리하고 있는데 보통 조금 바꿀때 마다 서버를 재구동 시켜야 되는 문제가 된다.
시스템이 클 경우 재시작시 오랜 시간 걸리고 session 사용시 또 로그인을 해야 하는듯 개발의 흐름이 끊어지는 문제가 많이 발생한다.
예전에 ibatis를 사용 했을시에도 그런 부분이 많이 불편했었는데, 예전 대우정보시스템의 JCF 프레임워크에서 사용된다고 Refresh 되는 클래스 소스가 한번 공개 된적이 있었다. ( 몇년전인지 기억은 안나지만, 당시 인터넷 검색으로 찾았었다. )
그것이 버전이 문제인지 바로 사용이 안되어서 커스터마이징하고 사용을 잘사용 했었다.
그런데 지금 프로젝트가 mybatis로 진행하기 때문에 예전과 같은 불편함이 또 생기게 되었는데, 이 번에는 그 소스를 mybatis에 맞도로 커스터마이징 하기로 했다.
일단 사전 조건은
JDK 1.5 이상, Spring, mybatis, spring-mybatis 라이브러리가 설치되어 있는 환경에서만 된다.
일단 기존 Spring 에서 mybatis 설정을 보겠다.
보통 sqlSessionFactory를 이렇게 설정 한다.
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:mapperLocations="classpath*:패키지경로/**/mapper.xml" p:configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource"/>
classpath*:패키지경로/**/mapper.xml <-이부분이 재로딩 될 xml 파일경로 ( 자신의 프로젝트에 따라 변경 )
이부분에서 굵게 표시한 class 부분만 새로 만든 클래스로 바꾸면 모든게 해결된다.
<bean id="sqlSessionFactory" class="패키지경로.RefreshableSqlSessionFactoryBean" p:mapperLocations="classpath*:패키지경로/**/mapper.xml" p:configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource" />
RefreshableSqlSessionFactoryBean.java
import java.io.IOException; | |
import java.lang.reflect.InvocationHandler; | |
import java.lang.reflect.Method; | |
import java.lang.reflect.Proxy; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Timer; | |
import java.util.TimerTask; | |
import org.apache.commons.logging.Log; | |
import org.apache.commons.logging.LogFactory; | |
import org.apache.ibatis.session.SqlSessionFactory; | |
import org.mybatis.spring.SqlSessionFactoryBean; | |
import org.springframework.beans.factory.DisposableBean; | |
import org.springframework.core.io.Resource; | |
import java.util.concurrent.locks.Lock; | |
import java.util.concurrent.locks.ReentrantReadWriteLock; | |
/** | |
* mybatis mapper 자동 감지 후 자동으로 서버 재시작이 필요 없이 반영 | |
* | |
* @author sbcoba | |
*/ | |
public class RefreshableSqlSessionFactoryBean extends SqlSessionFactoryBean implements DisposableBean { | |
private static final Log log = LogFactory.getLog(RefreshableSqlSessionFactoryBean.class); | |
private SqlSessionFactory proxy; | |
private int interval = 500; | |
private Timer timer; | |
private TimerTask task; | |
private Resource[] mapperLocations; | |
/** | |
* 파일 감시 쓰레드가 실행중인지 여부. | |
*/ | |
private boolean running = false; | |
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); | |
private final Lock r = rwl.readLock(); | |
private final Lock w = rwl.writeLock(); | |
public void setMapperLocations(Resource[] mapperLocations) { | |
super.setMapperLocations(mapperLocations); | |
this.mapperLocations = mapperLocations; | |
} | |
public void setInterval(int interval) { | |
this.interval = interval; | |
} | |
/** | |
* @throws Exception | |
*/ | |
public void refresh() throws Exception { | |
if (log.isInfoEnabled()) { | |
log.info("refreshing sqlMapClient."); | |
} | |
w.lock(); | |
try { | |
super.afterPropertiesSet(); | |
} finally { | |
w.unlock(); | |
} | |
} | |
/** | |
* 싱글톤 멤버로 SqlMapClient 원본 대신 프록시로 설정하도록 오버라이드. | |
*/ | |
public void afterPropertiesSet() throws Exception { | |
super.afterPropertiesSet(); | |
setRefreshable(); | |
} | |
private void setRefreshable() { | |
proxy = (SqlSessionFactory) Proxy.newProxyInstance( | |
SqlSessionFactory.class.getClassLoader(), | |
new Class[]{SqlSessionFactory.class}, | |
new InvocationHandler() { | |
public Object invoke(Object proxy, Method method, | |
Object[] args) throws Throwable { | |
// log.debug("method.getName() : " + method.getName()); | |
return method.invoke(getParentObject(), args); | |
} | |
}); | |
task = new TimerTask() { | |
private Map<Resource, Long> map = new HashMap<Resource, Long>(); | |
public void run() { | |
if (isModified()) { | |
try { | |
refresh(); | |
} catch (Exception e) { | |
log.error("caught exception", e); | |
} | |
} | |
} | |
private boolean isModified() { | |
boolean retVal = false; | |
if (mapperLocations != null) { | |
for (int i = 0; i < mapperLocations.length; i++) { | |
Resource mappingLocation = mapperLocations[i]; | |
retVal |= findModifiedResource(mappingLocation); | |
} | |
} | |
return retVal; | |
} | |
private boolean findModifiedResource(Resource resource) { | |
boolean retVal = false; | |
List<String> modifiedResources = new ArrayList<String>(); | |
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 (IOException e) { | |
log.error("caught exception", e); | |
} | |
if (retVal) { | |
if (log.isInfoEnabled()) { | |
log.info("modified files : " + modifiedResources); | |
} | |
} | |
return retVal; | |
} | |
}; | |
timer = new Timer(true); | |
resetInterval(); | |
} | |
private Object getParentObject() throws Exception { | |
r.lock(); | |
try { | |
return super.getObject(); | |
} finally { | |
r.unlock(); | |
} | |
} | |
public SqlSessionFactory getObject() { | |
return this.proxy; | |
} | |
public Class<? extends SqlSessionFactory> getObjectType() { | |
return (this.proxy != null ? this.proxy.getClass() | |
: SqlSessionFactory.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(); | |
} | |
} |
만약에 재로딩 되는 시간을 바꾸고 싶으면
<bean id="sqlSessionFactory" class="패키지경로.RefreshableSqlSessionFactoryBean" p:mapperLocations="classpath*:패키지경로/**/mapper.xml" :configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource" p:interval="1000" />
p:interval="1000" 이부분을 수치를 정해주면된다. ( 디폴트는 500, 단위 ms )
이제 설정을 서버를 시작해서 위에 로케이션 해당하는 mapper.xml ( sql이 있는 xml , 설정에 따라 다름)에서
sql을 바꿔 보자 그리고 클라이언트에서 바뀐지 확인해보자
큰 문제가 없다면 반영될것이라고 생각된다.
아 단 운영 시스템에 사용은 보장 못합니다~ 개발시에서만 사용하세요~
이 소스는 예전에 인터넷에서 나돌던 RefreshableSqlMapClientFactoryBean 소스를 커스터마이징한 소스인데 문제가 있다면 연락주시길바랍니다.
'FrameWork' 카테고리의 다른 글
[Spring, Log] 로그가 안보일때, 쿼리가 안나타날때 로그 출력하는 방법 (0) | 2019.03.23 |
---|---|
iBatis에서 AutoResultMap Error 해결방법 (0) | 2015.03.04 |
iBATIS 간단한 Tip.. (0) | 2012.10.17 |