티스토리 뷰
Springboot + MyBatis에서 Query 변경시 서버 재시작 없이 Reload 하기 (w. RefreshableSqlSessionFactoryBean)
호형 2021. 9. 23. 12:35이전에 Spring + MyBatis에서 Query 변경시 서버 재시작 없이 Reload 하기에 대한 글을 작성했었다.
Spring + Mybatis 에서 Query 변경시 서버 재시작 없이 Reload 하기
MyBatis 를 사용하여 개발하는 경우 쿼리를 수정했을때 WAS를 재기동해야 변경된 내용이 반영된다. 클래스가 몇개 없는 작은 프로젝트에서는 이렇게 재기동을 해서 확인을 하는것이 큰 부담이 없
oingdaddy.tistory.com
하지만 요즘 대세인 Springboot 환경에서 적용하기 위해서 Java Config로 이를 전환하였다. Xml Config에서 Java Config로 전환을 하는데 RefreshableSqlSessionFactoryBean을 그대로 사용하니 잘 동작하지 않았다.
그래서 RefreshableSqlSessionFactoryBean의 proxy를 초기화 시켜주는 부분을 조금 변경하였다.
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 java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.io.Resource;
public class RefreshableSqlSessionFactoryBean extends SqlSessionFactoryBean implements DisposableBean {
private static final Logger LOG = LoggerFactory.getLogger(RefreshableSqlSessionFactoryBean.class);
private SqlSessionFactory proxy;
private int interval = 1000;
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;
}
public void refresh() throws Exception {
w.lock();
try {
super.afterPropertiesSet();
} finally {
w.unlock();
}
LOG.info("sqlMapClient refreshed.");
}
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
}
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 {
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) {
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() throws Exception {
if(this.proxy == null) {
setRefreshable();
}
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();
}
}
여기서 139 line의 getObject() 해오는 부분에서 proxy에 값을 넣을수 있도록 변경을 해주고 67 line의 afterPropertiesSet에서는 setRefreshable()을 호출하는 부분을 삭제했다.
그리고 이것을 설정하는 Java Config는 다음과 같다.
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new RefreshableSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setVfs(SpringBootVFS.class);
factoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis/mybatis-config.xml"));
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resource = resolver.getResources("classpath*:mybatis/sql/**/*.xml");
factoryBean.setMapperLocations(resource);
((RefreshableSqlSessionFactoryBean) factoryBean).setInterval(1000);
return factoryBean.getObject();
}
이렇게 적당한 Config 클래스에 추가를 해주자. 물론 ConfigLocaction이라던지 MapperLocataion은 프로젝트에 맞게 변경을 해줘야 한다. Xml Config와 마찬가지로 interval 을 추가해서 갱신주기도 설정해 주도록 하자 (1000 = 1초)
이렇게 만든 sqlSessionFactory를 SqlSessionTemplate이라던지 CommonDao 같은 곳에 넣어서 사용하도록 하자.
설정이 다 되었으면 query를 변경한 후 저장을 누르면
이런식으로 쿼리가 변경된것을 감지하고 refresh 해주는것을 확인할 수 있다.
끝!
'Framework > Spring' 카테고리의 다른 글
Springboot STS Dashboard [unknown port] 현상 조치 (0) | 2021.09.27 |
---|---|
Springboot Profile 설정방법 및 가져오기 (1) | 2021.09.23 |
Spring + Mybatis 에서 Query 변경시 서버 재시작 없이 Reload 하기 (0) | 2021.09.17 |
Spring Boot Transaction Timeout 설정 및 기본값 (0) | 2021.09.02 |
Springboot + Ehcache 초간단 설정 및 사용방법 (0) | 2021.08.26 |