diff --git a/build.gradle b/build.gradle index 28c989d..d1b712c 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.17.0' } tasks.named('test') { diff --git a/settings.gradle b/settings.gradle index 8c0ebe3..2380fe2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'agent' +rootProject.name = 'dfxagent' diff --git a/src/docs/settings-examples/dfxagent.json b/src/docs/settings-examples/dfxagent.json new file mode 100644 index 0000000..2db8f28 --- /dev/null +++ b/src/docs/settings-examples/dfxagent.json @@ -0,0 +1,19 @@ +{ + "datasource": [ + { + "dataSourceId": "dfcms", + "driverClassName": "org.postgresql.Driver", + "url": "jdbc:postgresql://bsm-lab.com:5432/defree?currentSchema=DFCMS", + "username": "defreeadmin", + "password": "qortpals1!" + }, + { + "dataSourceId": "mochastory", + "driverClassName": "com.mysql.jdbc.Driver", + "url": "jdbc:mysql://bsm-lab.com:3306/MOCHASTORY?allowPublicKeyRetrieval=true", + "username": "MOCHASTORY", + "password": "MOCHASTORY" + } + ], + "sqlMapperLocations": ["D:/projects/bsm-lab/dfx/dfxagent/src/docs/mapper-examples/**/*.xml"] +} diff --git a/src/docs/settings-examples/dfxagent.yml b/src/docs/settings-examples/dfxagent.yml new file mode 100644 index 0000000..ba716e1 --- /dev/null +++ b/src/docs/settings-examples/dfxagent.yml @@ -0,0 +1,12 @@ +datasource: + dfcms: + driverClassName: org.postgresql.Driver + url: jdbc:postgresql://bsm-lab.com:5432/defree?currentSchema=DFCMS + username: defreeadmin + password: qortpals1! + mochastory: + driverClassName: com.mysql.jdbc.Driver + url: jdbc:mysql://bsm-lab.com:3306/MOCHASTORY?allowPublicKeyRetrieval=true + username: MOCHASTORY + password: MOCHASTORY + diff --git a/src/main/java/com/bsmlab/dfx/agent/config/DfxAgengConfiguration.java b/src/main/java/com/bsmlab/dfx/agent/config/DfxAgengConfiguration.java deleted file mode 100644 index 1eaa9c1..0000000 --- a/src/main/java/com/bsmlab/dfx/agent/config/DfxAgengConfiguration.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.bsmlab.dfx.agent.config; - -import com.fasterxml.jackson.databind.DatabindException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.env.YamlPropertySourceLoader; -import org.springframework.core.io.support.ResourcePropertySource; -import org.springframework.stereotype.Component; - -import java.io.File; -import java.io.IOException; -import java.util.Map; - -@Slf4j -@Component -public class DfxAgengConfiguration { - private String settingFilePath; - public void loadConfiguraion() { - //setting.file=D:\projects\bsm-lab\dfx\run\dfxagent.json - String embeddedDbFileDirectory = System.getProperty("embedded.db.file.directory"); - this.settingFilePath = System.getProperty("setting.file"); - try { - ObjectMapper mapper = new ObjectMapper(); - Map settingMap = mapper.readValue(new File(this.settingFilePath), Map.class); - } catch (DatabindException e) { - log.error("cannot parse a setting file. {}", this.settingFilePath, e); - } catch (IOException e) { - log.error("cannot found a setting file. {}", this.settingFilePath); - log.error("-Dsetting.file=[file path] needed."); - } - } - - public String getSettingFilePath() { - return this.settingFilePath; - } -} diff --git a/src/main/java/com/bsmlab/dfx/agent/config/DfxAgentConfiguration.java b/src/main/java/com/bsmlab/dfx/agent/config/DfxAgentConfiguration.java new file mode 100644 index 0000000..e2765ed --- /dev/null +++ b/src/main/java/com/bsmlab/dfx/agent/config/DfxAgentConfiguration.java @@ -0,0 +1,54 @@ +package com.bsmlab.dfx.agent.config; + +import com.bsmlab.dfx.agent.config.datasource.RefreshableSqlSessionFactoryBean; +import io.micrometer.common.util.StringUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class DfxAgentConfiguration { + public final ApplicationContext applicationContext; + + // java -jar dfxagent.jar --embedded.db.file.directory=D:\projects\bsm-lab\dfx\dfxagent\src\docs\settings-examples --setting.file=D:\projects\bsm-lab\dfx\dfxagent\src\docs\settings-examples\dfxagent.json + // embedded.db.file.directory=D:\projects\bsm-lab\dfx\dfxagent\src\docs\settings-examples + @Value("${embedded.db.file.directory}") + private String embeddedDbFileDirectory; + // setting.file=D:\projects\bsm-lab\dfx\dfxagent\src\docs\settings-examples\dfxagent.json + @Value("${setting.file}") + private String settingFile; + + @Bean("settings") + public Settings loadSettings() { + if(StringUtils.isBlank(this.embeddedDbFileDirectory)) { + log.error("cannot found a embedded DB file. {}", this.embeddedDbFileDirectory); + log.error("exit application"); + System.exit(0); + } + if(StringUtils.isBlank(this.settingFile)) { + log.error("cannot found a setting file. {}", this.settingFile); + log.error("exit application"); + System.exit(0); + } + Settings settings = new Settings(); + settings.loadSettingFile(this.settingFile); + return settings; + } + + @Bean + public SqlSessionFactoryBean sqlSessionFactoryBean () { + RefreshableSqlSessionFactoryBean sqlSessionFactoryBean = new RefreshableSqlSessionFactoryBean(); + //setMapperLocations(Resource[] mapperLocations) + Settings settings = applicationContext.getBean(Settings.class); + Resource[] mapperLocations = settings.getMapperLocations(); + sqlSessionFactoryBean.setMapperLocations(mapperLocations); + return sqlSessionFactoryBean; + } +} diff --git a/src/main/java/com/bsmlab/dfx/agent/config/Settings.java b/src/main/java/com/bsmlab/dfx/agent/config/Settings.java new file mode 100644 index 0000000..22094ca --- /dev/null +++ b/src/main/java/com/bsmlab/dfx/agent/config/Settings.java @@ -0,0 +1,69 @@ +package com.bsmlab.dfx.agent.config; + +import com.bsmlab.dfx.agent.config.datasource.DataSourceDto; +import com.fasterxml.jackson.databind.DatabindException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class Settings { + private Map dataSourceDtoMap = new HashMap<>(); + private Resource[] mapperLocations; + public void loadSettingFile(String settingFilePath) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + Map settingMap = objectMapper.readValue(new File(settingFilePath), Map.class); + this.mapperLocations = this.createMapperLocations(settingMap); + log.debug("settingMap: {}", settingMap); + this.parseDataSources(settingMap); + } catch (DatabindException e) { + log.error("cannot parse a setting file. {}", settingFilePath, e); + log.error(e.getMessage(), e); + } catch (IOException e) { + log.error("cannot read a setting file. {}", settingFilePath); + log.error(e.getMessage(), e); + } + } + + @SuppressWarnings("unchecked") + private void parseDataSources(Map settingMap) { + List> dataSourceMapList = (List>)settingMap.get("datasource"); + for(Map dataSourceMap : dataSourceMapList) { + DataSourceDto dataSourceDto = DataSourceDto.builder() + .dataSourceId(dataSourceMap.get("dataSourceId")).driverClassName(dataSourceMap.get("driverClassName")) + .url(dataSourceMap.get("url")).username(dataSourceMap.get("username")).password(dataSourceMap.get("password")) + .build(); + dataSourceDtoMap.put(dataSourceDto.getDataSourceId(), dataSourceDto); + } + } + + private Resource[] createMapperLocations(Map settingMap) throws FileNotFoundException { + Resource[] resources = null; + if(ObjectUtils.isNotEmpty(settingMap.get("mapperLocations"))) { + String[] locationStringList = (String[])settingMap.get("mapperLocations"); + resources = new Resource[locationStringList.length]; + int i = 0; + for(String location : locationStringList) { + resources[i] = new InputStreamResource(new FileInputStream(new File(location))); + } + } + return resources; + } + + public Resource[] getMapperLocations() { + return this.mapperLocations; + } +} diff --git a/src/main/java/com/bsmlab/dfx/agent/config/StartupRunner.java b/src/main/java/com/bsmlab/dfx/agent/config/StartupRunner.java index b79fbd8..8483e2e 100644 --- a/src/main/java/com/bsmlab/dfx/agent/config/StartupRunner.java +++ b/src/main/java/com/bsmlab/dfx/agent/config/StartupRunner.java @@ -17,7 +17,7 @@ public class StartupRunner implements ApplicationRunner { log.debug("run"); //TODO 1. dfxagent.yml 로드 및 설정 관리자에 저장 //TODO 2. embedded db 체크 및 생성 - embeddedDbInitializer.copyEmbeddedDbFileIfNotExists(); + //embeddedDbInitializer.copyEmbeddedDbFileIfNotExists(); //TODO 3. dfxagent.yml 을 기반으로 데이터소스 및 커넥션 풀 생성 //TODO 4. Worker 쓰레드 생성 diff --git a/src/main/java/com/bsmlab/dfx/agent/config/UserService.java b/src/main/java/com/bsmlab/dfx/agent/config/UserService.java index fdc220c..66275d1 100644 --- a/src/main/java/com/bsmlab/dfx/agent/config/UserService.java +++ b/src/main/java/com/bsmlab/dfx/agent/config/UserService.java @@ -1,24 +1,25 @@ package com.bsmlab.dfx.agent.config; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class UserService { - - @Autowired + private final DynamicRoutingDataSource dynamicRoutingDataSource; private UserMapper userMapper; public void fetchUsersFromDefault() { - DynamicRoutingDataSource.setDataSource("default"); + dynamicRoutingDataSource.setDataSource("default"); System.out.println("🔹 Default DB에서 데이터 조회"); System.out.println(userMapper.findAll()); } @Transactional public void fetchUsersFromNewDB() { - DynamicRoutingDataSource.setDataSource("newDB"); + dynamicRoutingDataSource.setDataSource("newDB"); System.out.println("🔹 New DB에서 데이터 조회"); System.out.println(userMapper.findAll()); } diff --git a/src/main/java/com/bsmlab/dfx/agent/config/datasource/DataSourceDto.java b/src/main/java/com/bsmlab/dfx/agent/config/datasource/DataSourceDto.java new file mode 100644 index 0000000..db11a41 --- /dev/null +++ b/src/main/java/com/bsmlab/dfx/agent/config/datasource/DataSourceDto.java @@ -0,0 +1,18 @@ +package com.bsmlab.dfx.agent.config.datasource; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DataSourceDto { + private String dataSourceId; + private String driverClassName; + private String url; + private String username; + private String password; +} diff --git a/src/main/java/com/bsmlab/dfx/agent/config/datasource/DataSourcePool.java b/src/main/java/com/bsmlab/dfx/agent/config/datasource/DataSourcePool.java new file mode 100644 index 0000000..7e9b251 --- /dev/null +++ b/src/main/java/com/bsmlab/dfx/agent/config/datasource/DataSourcePool.java @@ -0,0 +1,9 @@ +package com.bsmlab.dfx.agent.config.datasource; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class DataSourcePool { +} diff --git a/src/main/java/com/bsmlab/dfx/agent/config/datasource/RefreshableSqlSessionFactoryBean.java b/src/main/java/com/bsmlab/dfx/agent/config/datasource/RefreshableSqlSessionFactoryBean.java new file mode 100644 index 0000000..c6d6c86 --- /dev/null +++ b/src/main/java/com/bsmlab/dfx/agent/config/datasource/RefreshableSqlSessionFactoryBean.java @@ -0,0 +1,180 @@ +package com.bsmlab.dfx.agent.config.datasource; + +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Slf4j +@Component +@DependsOn(value = {"settings"}) +public class RefreshableSqlSessionFactoryBean extends SqlSessionFactoryBean implements DisposableBean { + 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; + } + + public void refresh() throws Exception { + if(log.isInfoEnabled()) { + log.info("> Refresh SqlMapper"); + log.info("======================================================================================"); + } + w.lock(); + try { + super.afterPropertiesSet(); + } finally { + w.unlock(); + } + } + + @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 { + return method.invoke(getParentObject(), args); + } + }); + + task = new TimerTask() { + private Map map = new HashMap<>(); + + 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 modifiedResources = new ArrayList<>(); + 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()); // 전체경로 + modifiedResources.add(resource.getFilename()); // 파일명 + + retVal = true; + } + } else { + map.put(resource, modified); + } + } catch (IOException e) { + log.error("caught exception", e); + } + if(retVal) { + if(log.isInfoEnabled()) { + log.info("======================================================================================"); + log.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(); + } + } + + @Override + public SqlSessionFactory getObject() { + return this.proxy; + } + + @Override + public Class 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; + } + } + + @Override + public void destroy() throws Exception { + timer.cancel(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2176360..f679982 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,4 +9,7 @@ spring: url: jdbc:h2:classpath:/storages/dfxagent username: smbaek password: qortpals1! +logging: + level: + com.bsmlab.dfx.agent: DEBUG diff --git a/src/test/java/com/bsmlab/dfx/agent/DfxAgentApplicationTests.java b/src/test/java/com/bsmlab/dfx/agent/DfxAgentApplicationTests.java index 69b63f6..dbcfaa9 100644 --- a/src/test/java/com/bsmlab/dfx/agent/DfxAgentApplicationTests.java +++ b/src/test/java/com/bsmlab/dfx/agent/DfxAgentApplicationTests.java @@ -1,13 +1,14 @@ package com.bsmlab.dfx.agent; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class DfxAgentApplicationTests { +// @Autowired +// private DfxAgengConfiguration dfxAgengConfiguration; - @Test - void contextLoads() { - } +// @Test +// void contextLoads() { +// } }