- Arawn's Dev Blog
- Outsider's Dev Story
- Toby's Epril
- Benelog
- NHN 개발자 블로그
- SK 플래닛 기술 블로그
- OLC CENTER
- 소프트웨어 경영/공학 블로그
- 모바일 컨버전스
- KOSR - Korea Operating System …
- 넥스트리 블로그
- 리버스코어 ReverseCore
- SLiPP
- 개발자를 위하여... (Nextree 임병인 수석)
- "트위터 부트스트랩: 디자이너도 놀라워할 매끈하고 직관…
- Learning English - The English…
- real-english.com
- 'DataScience/Deep Learning' 카테…
- Deep Learning Summer School, M…
- Deep Learning Courses
민서네집
[mybatis] mapper xml 파일을 서버 재시작 없이 reloading 하기. 본문
위 블로그를 참조했는데,
SqlSessionFactoryBean 을 설정할 때, mapperLocations 를 주지 않고, 아래처럼 configLocation 을 설정하는 경우 xml 파일이 relaoding 되지 않았다.
<!-- define the SqlSessionFactory -->
<!-- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> -->
<bean id="sqlSessionFactory" class="com.example.util.RefreshableSqlSessionFactoryBean"> <!-- 서버 재시작 없이 mybatis mapper xml 파일을 reloading 하는 클래스 -->
<property name="dataSource" ref="dataSource" />
<property name="typeAliasesPackage" value="com.example.dao" />
<property name="configLocation" value="classpath:mapper/MapperConfig.xml" />
<property name="interval" value="2000" /> <!-- mapper xml 파일을 재로딩 하는 간격 -->
</bean>
그래서 위 블로그를 참고로 해서 좀 수정했다.
configLocation 에 지정된 MapperConfig.xml 파일 안에 지정된 다른 mapper 파일을 불러오기 위해서, xml 파일을 parsing 했고, protected field 값을 가져오기 위해 reflection 을 사용했다.
그리고, mybatis-spring-1.1.0.jar 파일 안의 SqlSessionFactoryBean.java 를 참고했다.
RefreshableSqlSessionFactoryBean.java 의 소스를 보면 알겠지만, SqlSessionFactoryBean 클래스를 상속받아 만들므로 mybatis-spring 의 버전과는 상관없이 사용할 수 있을 듯 하다. - refresh() 할때마다 afterPropertiesSet()을 호출하고, SqlSessionFactoryBean 클래스의 소스를 보면 buildSqlSessionFactory() 메서드가 호출되서 sqlSessionFactory 의 값을 바꾼다.
내가 위 블로그를 참조해서 약간 수정한 소스는 다음과 같다.
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 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 configLocation; private Resource[] mapperLocations; private Properties configurationProperties; /** * Set optional properties to be passed into the SqlSession configuration, as alternative to a * {@code <properties>} 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<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); 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<String> loadedResources = (Set<String>) loadedResourcesField.get(configuration); for (Iterator<String> 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<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(); } }
위 소스에서 SqlSessionFactory 를 proxy 로 생성하면서, invocationHandler 를 등록하였는데, SqlSessionFactory 의 메서드를 호출할 때마다 invocationHandler 가 호출된다. 이것을 이용하면 요청이 없을 때도 주기적으로 xml 파일이 변경되었는지 scan 하지 않고, Spring Framework 의 org.springframework.context.support.ReloadableResourceBundleMessageSource 클래스처럼 요청이 왔을 때만 마지막으로 검사한 시각과 현재 시각과 비교해서 시간 간격이 지정된 시간 이내면 다시 scan 하지 않고, 메모리에 있는 것으로 처리하게 할 수도 있을 것이다.
한가지 주의할 점은 RefreshableSqlSessionFactoryBean 을 설정할 때 mapperLocations 없이 configLocation 만 지정하였는데도, 서버가 시작할 때는 configLocation 으로 지정된 xml 파일 안에 mapper xml 파일을 지정하지 않아도 mapper 파일 안의 sql 이 잘 실행되지만, RefreshableSqlSessionFactoryBean 이 refresh 되고 나서는 이 config 파일 안에 지정되지 않은 mapper xml 파일 안의 sql 은 실행되지 않는다는 점이다. (정확히 말하면, mapper 파일 안의 쿼리 statement 를 가지고 있지 않아서 에러가 난다.)
이유를 잘 모르겠지만, 아마도 서버를 시작할 때 다음과 같이 MapperScannerConfigurer 설정이 되어 있어서 configLocation 에 지정되어 있지 않아도 지정된 package 밑의 모든 mapper 파일을 찾아서 쿼리 statement 를 모두 가지고 있는 듯 하다.
<!-- scan for mappers and let them be autowired -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.dao" />
</bean>
참고로 configLocation 에 지정된 MapperConfig.xml 파일은 다음과 같다.
<?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>
<bean id="sqlSessionFactory" class="패키지경로.RefreshableSqlSessionFactoryBean" p:mapperLocations="classpath*:패키지경로/**/mapper.xml" p:configLocation="classpath:/MapperConfig.xml" p:dataSource-ref="dataSource" />
아니면 위에서처럼 configLocation 과 mapperLocations 를 모두 다 주고, RefreshableSqlSessionFactoryBean 에서는 configLocations 에 지정된 mapper 파일은 무시하고, mapperLocations 에 지정된 mapper 파일만 변경 여부를 검사하는게 더 나을 수도 있겠다. - 파일을 중복해서 검사하지 않도록.
아직 테스트 해 보지는 않았는데, mapper 파일이 수정되는게 아니라 아예 없던 파일이 생성되는 경우도 서버 재시작 없이 적용 가능할까? mapperLocations 를
p:mapperLocations="classpath*:패키지경로/**/mapper.xml"
이렇게 설정하면 될까? 안된다면 configLocation 에 지정된 MapperConfig.xml 파일에 mapper 파일을 추가하고, MapperConfig.xml 파일이 변경되는지도 체크하도록 RefreshableSqlSessionFactoryBean 을 약간 변경할 수 있을것 같다.
< 2014-06-05 업데이트 >
sqlSessionFactory bean 설정에서 mapperLocations 속성이 주어지는 경우는
isModified() 메서드에서 configLocation 속성으로 주어진 파일에서 mapper 파일을 검사하지 않도록 수정함.
이유: configLocation 파일 안의 mapper 파일을 체크하기 위해서는 reflection 을 사용하기 때문에
성능 저하가 일어나지 않도록.
pom.xml 파일에서 다음과 같이 설정하고,
<!-- cache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.8.2</version>
<type>pom</type>
</dependency>
<!-- myBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.7</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>
spring database 설정
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="user" />
<property name="password" value="password" />
</bean>
<context:annotation-config />
<tx:annotation-driven />
<!-- define the SqlSessionFactory -->
<!-- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> -->
<bean id="sqlSessionFactory" class="com.example.config.mybatis.RefreshableSqlSessionFactoryBean"> <!-- 서버 재시작 없이 mybatis mapper xml 파일을 reloading 하는 클래스 -->
<property name="dataSource" ref="dataSource" />
<property name="typeAliasesPackage" value="com.example" />
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml" />
<property name="interval" value="5000" /> <!-- mapper xml 파일을 재로딩 하는 간격 -->
<property name="mapperLocations" value="classpath:com/example/**/dao/*Mapper.xml" />
</bean>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory" />
</bean>
<!-- scan for mappers and let them be autowired -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource" />
</beans>
위와 같이 설정해서 정상적으로 mapper xml 파일의 변경사항이 서버 재시작 없이 적용되는 것을 확인하였다.
'Spring' 카테고리의 다른 글
Thymeleaf (0) | 2013.08.06 |
---|---|
RestTemplate 이용하여 개발시 유용한 크롬 플러그인 - Postman (0) | 2013.07.25 |
[spring] 어느 곳에서나 request 를 얻기. (0) | 2013.07.09 |
Transaction 설정 파일 (0) | 2013.07.05 |
WebApplication 에서 Spring Bean 가져오기 (0) | 2013.05.06 |