Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save composite/27de4a82397fdb03b38c22a6ffaa64a5 to your computer and use it in GitHub Desktop.
Save composite/27de4a82397fdb03b38c22a6ffaa64a5 to your computer and use it in GitHub Desktop.
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();
}
}
package com.example.util;
import java.io.IOException;
import java.lang.reflect.Field;
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.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
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.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
/**
* mybatis mapper 자동 감지 후 자동으로 서버 재시작이 필요 없이 반영
*
* [참고] http://sbcoba.tistory.com/entry/Spring-mybats-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%9E%AC%EC%8B%9C%EC%9E%91-%EC%97%86%EC%9D%B4-%EC%84%9C%EB%B2%84-%EB%B0%98%EC%98%81
*/
public class RefreshableSqlSessionFactoryBean2 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 configLocation;
private Resource[] mapperLocations;
private Properties configurationProperties;
/**
* Set optional properties to be passed into the SqlSession configuration, as alternative to a
* {@code &amp;lt;properties&amp;gt;} tag in the configuration xml file. This will be used to
* resolve placeholders in the config file.
*/
public void setConfigurationProperties(Properties sqlSessionFactoryProperties) {
super.setConfigurationProperties(sqlSessionFactoryProperties);
this.configurationProperties = sqlSessionFactoryProperties;
}
/**
* 파일 감시 쓰레드가 실행중인지 여부.
*/
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.configLocation = configLocation;
}
public void setMapperLocations(Resource[] mapperLocations)
{
super.setMapperLocations(mapperLocations);
this.mapperLocations = mapperLocations;
}
public void setInterval(int interval)
{
this.interval = interval;
}
public void refresh() throws Exception
{
if (log.isInfoEnabled())
{
log.info("refreshing SqlSessionFactory.");
}
w.lock();
try
{
super.afterPropertiesSet();
} finally
{
w.unlock();
}
}
/**
*
* 싱글톤 멤버로 SqlSessionFactory 원본 대신 프록시로 설정하도록 오버라이드.
*/
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&lt;Resource, Long&gt; map = new HashMap&lt;Resource, Long&gt;();
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 &lt; mapperLocations.length; i++)
{
Resource mappingLocation = mapperLocations[i];
retVal |= findModifiedResource(mappingLocation);
if( retVal )
break;
}
}
else if( configLocation != null )
{
Configuration configuration = null;
XMLConfigBuilder xmlConfigBuilder = null;
try
{
xmlConfigBuilder = new XMLConfigBuilder(configLocation.getInputStream(), null, configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} catch (IOException e)
{
e.printStackTrace();
}
if (xmlConfigBuilder != null) {
try {
xmlConfigBuilder.parse();
// Configuration 클래스의 protected member field 인 loadedResources 를 얻기 위해 reflection 을 사용함.
Field loadedResourcesField = Configuration.class.getDeclaredField("loadedResources");
loadedResourcesField.setAccessible(true);
@SuppressWarnings("unchecked")
Set&lt;String&gt; loadedResources = (Set&lt;String&gt;) loadedResourcesField.get(configuration);
for (Iterator&lt;String&gt; iterator = loadedResources.iterator(); iterator.hasNext();)
{
String resourceStr = (String) iterator.next();
if( resourceStr.endsWith(".xml") ) {
Resource mappingLocation = new ClassPathResource(resourceStr);
retVal |= findModifiedResource(mappingLocation);
if( retVal ) {
break;
}
}
}
} catch (Exception ex) {
throw new RuntimeException("Failed to parse config resource: " + configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}
}
return retVal;
}
private boolean findModifiedResource(Resource resource)
{
boolean retVal = false;
List&lt;String&gt; modifiedResources = new ArrayList&lt;String&gt;();
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&lt;? extends SqlSessionFactory&gt; 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 &gt; 0)
{
timer.schedule(task, 0, interval);
running = true;
}
}
public void destroy() throws Exception
{
timer.cancel();
}
}
@composite
Copy link
Author

composite commented Jul 24, 2019

RefreshableSqlSessionFactoryBean 세팅

class="org.mybatis.spring.SqlSessionFactoryBean" 부분을
class="패키지경로.RefreshableSqlSessionFactoryBean" 로 바꾸면 끝.
파일 모니터리 간격을 바꿀 때 interval="(밀리초 숫자)" 설정.

<bean id="sqlSessionFactory" class="패키지경로.RefreshableSqlSessionFactoryBean"
		p:mapperLocations="classpath*:패키지경로/**/mapper.xml"
                p:configLocation="classpath:/MapperConfig.xml"
                p:dataSource-ref="dataSource"
                p:interval="1000" />

출처: https://sbcoba.tistory.com/16

@composite
Copy link
Author

RefreshableSqlSessionFactoryBean2 세팅

이 버전은 mapperLocations 속성이 없을 때 (주로 mapper config 내 mapper 정의)를 대응한 버전.

mappconfig 파일이 아래와 같을 때,

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="multipleResultSetsEnabled" value="true"/>
        <setting name="useColumnLabel" value="true"/>
        <setting name="defaultStatementTimeout" value="25000"/>
    </settings>

    <mappers>
        <!-- Apps Mapper File -->
        <mapper resource="com/example/dao/apps/AppsMapper.xml"/>
        
        <!-- Common Mapper File -->
        <mapper resource="com/example/dao/common/CommonMapper.xml"/>
        
        <!-- User Mapper File -->
        <mapper resource="com/example/dao/user/UserMapper.xml"/>
    </mappers>

</configuration>

config는 아래와 같이 설정할 수 있다.

        <bean id="sqlSessionFactory" class="패키지경로.RefreshableSqlSessionFactoryBean2"
                p:configLocation="classpath:/MapperConfig.xml"
                p:dataSource-ref="dataSource"
                p:interval="1000" />

	<!-- scan for mappers and let them be autowired -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.example.dao" />
	</bean>

출처: https://bryan7.tistory.com/117

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment