티스토리 뷰
Springboot + MyBatis에서 Query 변경시 서버 재시작 없이 Reload 하기 (w. RefreshableSqlSessionFactoryBean)
호형 2021. 9. 23. 12:35이전에 Spring + MyBatis에서 Query 변경시 서버 재시작 없이 Reload 하기에 대한 글을 작성했었다.
하지만 요즘 대세인 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 |