Framework/Spring
[Spring Framework]MyBatis 서버 재실행없이 XML 적용하기(RefreshableSqlSessionFactoryBean)
SHXL2
2020. 12. 2. 23:47
반응형
Spring + MyBatis 환경에서 개발하다보면 xml 파일을 수정할 때마다 서버를 재실행해주어야하는 불편함을 느끼셨을 겁니다. 아마도 가장 큰 불편함은 특정 페이지를 개발하다가 서버를 재실행하면 세션이 끊겨 다시 그 페이지로 가는 과정(+또는 행동)을 거쳐야하는 게 아닐까 싶습니다.
본 포스팅에서는 xml 파일 수정시 서버의 재실행없이 반영되게 하는 방법을 정리해보았습니다.
※ 테스트 환경
- jdk 1.8
- Eclipse 2019-06
- Spring Framework 4.3
- MyBatis 3.4.6
- MariaDB
설정을 하기 앞서 Class 파일이 필요하다.
아래 소스를 원하는 패키지 경로에 추가한다.
RefreshableSqlSessionFactoryBean.java
package com.planm.util;
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.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;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RefreshableSqlSessionFactoryBean extends SqlSessionFactoryBean implements DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(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;
}
public void refresh() throws Exception {
if(logger.isInfoEnabled()) {
logger.info("> Refresh SqlMapper");
logger.info("======================================================================================");
}
w.lock();
try {
super.afterPropertiesSet();
} finally {
w.unlock();
}
}
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) {
logger.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()); // 전체경로
modifiedResources.add(resource.getFilename()); // 파일명
retVal = true;
}
} else {
map.put(resource, new Long(modified));
}
} catch (IOException e) {
logger.error("caught exception", e);
}
if(retVal) {
if(logger.isInfoEnabled()) {
logger.info("======================================================================================");
logger.info("> Update File name : " + 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();
}
}
현재 테스트 중인 프로젝트 구조는 아래 이미지와 같이 단순하다.
/WEB-INF/spring/root-context.xml 에서 JDBC와 DB 커넥션 설정을 함께 했다.
root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<!--
<property name="driverClassName" value="org.mariadb.jdbc.Driver" />
<property name="url" value="jdbc:mariadb://127.0.0.1:3306/shxdb" />
-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy" />
<property name="url" value="jdbc:log4jdbc:mysql://127.0.0.1:3306/shxdb" />
<property name="username" value="root" />
<property name="password" value="1234" />
</bean>
<!-- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> -->
<bean id="sqlSessionFactory" class="com.planm.util.RefreshableSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:/mybatis-config.xml" />
<property name="mapperLocations" value="classpath:/mapper/**/*.xml" />
</bean>
<!-- MyBatis - Spring 연동 모듈 -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
</beans>
설정 파일을 보면 기존에 SqlSessionFacotyrBean 객체를 추가된 RefreshableSqlSessionFactoryBean으로 변경해주었다.
<!-- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> -->
<bean id="sqlSessionFactory" class="com.planm.util.RefreshableSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:/mybatis-config.xml" />
<property name="mapperLocations" value="classpath:/mapper/**/*.xml" />
<property name="interval" value="1000" /> <!-- Mapper 파일 리로딩 간격(ms단위) -->
</bean>
적용이 잘되었다면 Mapper 파일을 수정할 때마다 아래와같은 로그를 확인할 수 있을 것이다.
참고)
반응형