[Jmlee] Spring Framework, Tomcat, myBatis, Hot Deploy, Hot Swapping ***

▦ 프로그램/└ Spring 2018. 4. 18. 16:35


Tomcat configuration


server.xml

<?xml version='1.0' encoding='utf-8'?>

<Server port="8005" shutdown="SHUTDOWN">

  <Service name="Catalina">

    <Connector ... />

    <Engine name="Catalina" defaultHost="localhost">

      <Realm ... />


      <Host name="localhost"

            appBase="webapps"

            unpackWARs="true"

            autoDeploy="false"

            >

...

  </Service>

</Server>



Eclipse configuration


C:/x/y/z/ 경로에 springloaded-1.2.5.RELEASE.jar 다운로드 (https://github.com/spring-projects/spring-loaded)


Window > Preferences > Tomcat > JVM Settings - Append to JVM Parameters 에 아래 옵션 추가

-javaagent:C:/x/y/z/springloaded-1.2.5.RELEASE.jar -noverify



Spring configuration


sql-map-config-xxx.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>

    ...

  </settings>


  <typeAliases>

    ...

  </typeAliases>


  <!-- context-mapper.xml 설정파일로 이동

  <mappers>

    ...

  </mappers>

  -->

</configuration>


context-mapper.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"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

                           http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">


  <bean id="sqlSessionFactory" class="x.y.z.RefreshableSqlSessionFactoryBean">

    <property name="configLocation" value="classpath:/egovframework/sqlmap/config/sql-map-config-xxx.xml" />

    <property name="mapperLocations" value="classpath:/egovframework/sqlmap/xxx/**/*Mapper*.xml" />

    <property name="interval" value="1000" />

    <property name="dataSource" ref="dataSource" />

  </bean>


  <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">

    <constructor-arg index="0" ref="sqlSessionFactory" />

  </bean>


</beans>


RefreshableSqlSessionFactoryBean.java (https://gist.github.com/sbcoba/a51a66a64d3441a88558)

package x.y.z;


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;


/**

 * mybatis mapper 자동 감지 후 자동으로 서버 재시작이 필요 없이 반영

 *

 * @author sbcoba

 */

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();


    @Override

    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 (logger.isInfoEnabled()) {

            logger.info("refreshing sqlMapClient.");

        }

        w.lock();

        try {

            super.afterPropertiesSet();


        } finally {

            w.unlock();

        }

    }


    /**

     * 싱글톤 멤버로 SqlMapClient 원본 대신 프록시로 설정하도록 오버라이드.

     */

    @Override

    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 {

                                // logger.debug("method.getName() : " + method.getName());

                                return method.invoke(getParentObject(), args);

                            }

                        });


        task = new TimerTask() {

            private Map<Resource, Long> map = new HashMap<Resource, Long>();


            @Override

            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());

                            retVal = true;

                        }

                    } else {

                        map.put(resource, new Long(modified));

                    }

                } catch (IOException e) {

                    logger.error("caught exception", e);

                }

                if (retVal) {

                    if (logger.isInfoEnabled()) {

                        logger.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();

        }

    }


    @Override

    public SqlSessionFactory getObject() {

        return this.proxy;

    }


    @Override

    public Class<? extends SqlSessionFactory> getObjectType() {

        return (this.proxy != null ? this.proxy.getClass() : SqlSessionFactory.class);

    }


    @Override

    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();

    }

}

: