springboot mybatis xml reload for intellij

leeminsu·2024년 3월 4일
  1. RefreshableSqlSessionFactoryBean class 를 생성한다.

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 = 100;
    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, modified);
                            modifiedResources.add(resource.getDescription());
                            retVal = true;
                        }
                    } else {
                        map.put(resource, 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();
    }
}

2.MyBatisConfig class를 생성한다.


import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * Mybatis 의 XML에서 쿼리 변경 시 was 재기동 없이 xml을 reload 하는 class
 */
@Configuration
@MapperScan(basePackages = "com.kona.framework.dao") // Mapper 인터페이스가 있는 패키지 경로 설정
public class MyBatisConfig {

    @Resource
    ApplicationContext applicationContext;

    /**
     * local 프로필에서만 사용하는 xml 파일 변경시 was 재기동 없이 refresh 하는 메서드
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Profile("local")
    @Bean
    public SqlSessionFactory refreshableSqlSessionFactory(DataSource dataSource) throws Exception {

        SqlSessionFactoryBean factoryBean = new RefreshableSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setVfs(SpringBootVFS.class);
        factoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis-config.xml"));
        factoryBean.setMapperLocations(applicationContext.getResources("classpath:mybatis/mapper/mariadb/*.xml"));
        factoryBean.setTypeAliasesPackage("com.kona.framework.domain.entity");
        ((RefreshableSqlSessionFactoryBean) factoryBean).setInterval(100);

        return factoryBean.getObject();
    }

    /**
     * xml 파일 reload는 local에서 개발용으로만 쓰고 그외는 일반적인 sqlsessionFactory를 사용한다.
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean
    @Profile("!local")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setConfigLocation(applicationContext.getResource("classpath:mybatis-config.xml"));
        bean.setMapperLocations(applicationContext.getResources("classpath:mybatis/mapper/mariadb/*.xml"));
        bean.setTypeAliasesPackage("com.kona.framework.domain.entity");

        return bean.getObject();
    }
}

3.devtools 적용

pom.xml

<!--hot reload devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

application-local.yml에 devtools 적용

spring:
  devtools:
    livereload:
      enabled: true # resource 수정시 재기동 옵션
    restart:
      enabled: false # classpath 에 속한 파일 Ex. class 수정시 재기동 옵션

4.intellij auto compile 저굥
4.1 Compiler > Build project automatically

4.2 Advanced Settings > Allow auto-make to start even if developed application is currently running

  1. console 결과물
profile
프로게이머 아니고 프로그래머

0개의 댓글