Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d8871eab0 | |||
| 727aa532c6 | |||
| 7b74b35794 | |||
| 2cd4eeabd2 | |||
| 4a05256c59 | |||
| 6f88950359 | |||
| a8e82b89f8 | |||
| 1bb78644d8 | |||
| fcc8d359c0 | |||
| 379f5c7032 | |||
| afa47c3ff3 | |||
| 39ac8e7b9f | |||
| 7f727b544e | |||
| 1bcdabc595 | |||
| b3c79f49a0 | |||
| 79216c27bb | |||
| 7ea0fc57b8 | |||
| ac852b9501 | |||
| e5061c1233 | |||
| c93309a7d2 | |||
| 4b974528b1 | |||
| 922135c3a7 | |||
| 42774eed50 | |||
| b3ca9c2647 | |||
| 05b637b9f1 | |||
| c2b71736cc | |||
| a0b0764ac3 | |||
| aa934121dc | |||
| 048c358800 | |||
| c7ea252159 | |||
| ac81e48d9c | |||
| 9900ccf1f5 | |||
| f66390e86b | |||
| a729a5d99c | |||
|
|
d0e1792e4b | ||
| fe86653903 | |||
| eefd907866 | |||
| f70db6ef90 | |||
| d929975809 |
23
pom.xml
23
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.serliunx.ddns</groupId>
|
||||
<artifactId>ddns-manager-lite</artifactId>
|
||||
<version>1.0.2</version>
|
||||
<version>1.0.3-alpha</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
@@ -19,6 +19,8 @@
|
||||
<snakeyaml.version>1.30</snakeyaml.version>
|
||||
<aliyundns.sdk.version>3.0.14</aliyundns.sdk.version>
|
||||
<tencent.dnspod.sdk.version>3.1.1002</tencent.dnspod.sdk.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
<sqlite.jdbc.version>3.47.0.0</sqlite.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -55,9 +57,26 @@
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- sqlite-jdbc -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>${sqlite.jdbc.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.jline/jline -->
|
||||
<dependency>
|
||||
<groupId>org.jline</groupId>
|
||||
<artifactId>jline</artifactId>
|
||||
<version>3.28.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jul-to-slf4j</artifactId>
|
||||
<version>1.7.36</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
package com.serliunx.ddns;
|
||||
|
||||
import com.serliunx.ddns.config.CommandLineConfiguration;
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.config.PropertiesConfiguration;
|
||||
import com.serliunx.ddns.config.listener.IpRefreshIntervalListener;
|
||||
import com.serliunx.ddns.config.listener.NotificationConfigListener;
|
||||
import com.serliunx.ddns.constant.SystemConstants;
|
||||
import com.serliunx.ddns.core.context.FileInstanceContext;
|
||||
import com.serliunx.ddns.core.context.MultipleSourceInstanceContext;
|
||||
import com.serliunx.ddns.support.InstanceContextHolder;
|
||||
import com.serliunx.ddns.support.SystemInitializer;
|
||||
import com.serliunx.ddns.support.okhttp.HttpClient;
|
||||
import com.serliunx.ddns.support.command.CommandCompleter;
|
||||
import com.serliunx.ddns.support.command.CommandDispatcher;
|
||||
import com.serliunx.ddns.support.command.target.HelpCommand;
|
||||
import com.serliunx.ddns.support.command.target.ReloadCommand;
|
||||
import com.serliunx.ddns.support.command.target.StopCommand;
|
||||
import com.serliunx.ddns.support.command.target.config.ConfigCommand;
|
||||
import com.serliunx.ddns.support.command.target.instance.InstanceCommand;
|
||||
import com.serliunx.ddns.support.log.JLineAdaptAppender;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.LineReaderBuilder;
|
||||
import org.jline.reader.impl.history.DefaultHistory;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.TerminalBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 启动类
|
||||
@@ -17,6 +38,11 @@ import com.serliunx.ddns.support.okhttp.HttpClient;
|
||||
*/
|
||||
public final class ManagerLite {
|
||||
|
||||
/**
|
||||
* 默认的日志输出
|
||||
*/
|
||||
private static final Logger DEFAULT_LOGGER = LoggerFactory.getLogger(ManagerLite.class);
|
||||
|
||||
/**
|
||||
* 配置信息
|
||||
*/
|
||||
@@ -29,20 +55,97 @@ public final class ManagerLite {
|
||||
* 系统初始化器
|
||||
*/
|
||||
private static SystemInitializer systemInitializer;
|
||||
/**
|
||||
* 指令调度
|
||||
*/
|
||||
private static CommandDispatcher commandDispatcher;
|
||||
|
||||
/**
|
||||
* 获取默认的日志输出
|
||||
*/
|
||||
public static Logger getLogger() {
|
||||
return DEFAULT_LOGGER;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 初始化slf4j日志桥接
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
|
||||
// 配置初始化
|
||||
initConfiguration();
|
||||
|
||||
// 相关工具初始化
|
||||
initTools();
|
||||
initConfiguration(args);
|
||||
|
||||
// 初始化实例容器
|
||||
initContext();
|
||||
|
||||
// 系统初始化
|
||||
initSystem();
|
||||
|
||||
// 配置监听器初始化
|
||||
initConfigurationListeners();
|
||||
|
||||
// 指令初始化
|
||||
initCommands();
|
||||
|
||||
Terminal terminal;
|
||||
try {
|
||||
terminal = TerminalBuilder.builder()
|
||||
.system(true)
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
// 不应该发生
|
||||
System.exit(0);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
LineReader lineReader = LineReaderBuilder.builder()
|
||||
.terminal(terminal)
|
||||
.completer(new CommandCompleter(commandDispatcher))
|
||||
// 如果想记录历史命令,可以配置一个 History
|
||||
.history(new DefaultHistory())
|
||||
.build();
|
||||
|
||||
JLineAdaptAppender.setLineReader(lineReader);
|
||||
|
||||
final String prompt = "client> ";
|
||||
|
||||
InstanceContextHolder.setAdditional("command-process");
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
String cmd = lineReader.readLine(prompt);
|
||||
commandDispatcher.onCommand(cmd);
|
||||
terminal.flush();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置监听器初始化
|
||||
*/
|
||||
private static void initConfigurationListeners() {
|
||||
// 配置监听器:IP更新间隔变动
|
||||
configuration.addListener(new IpRefreshIntervalListener(systemInitializer.getScheduledProvider()));
|
||||
// 配置监听器:通知变更
|
||||
configuration.addListener(new NotificationConfigListener());
|
||||
}
|
||||
|
||||
/**
|
||||
* 指令初始化
|
||||
*/
|
||||
private static void initCommands() {
|
||||
commandDispatcher = CommandDispatcher.getInstance();
|
||||
// help
|
||||
commandDispatcher.register(new HelpCommand());
|
||||
// reload
|
||||
commandDispatcher.register(new ReloadCommand(configuration, systemInitializer));
|
||||
// config
|
||||
commandDispatcher.register(new ConfigCommand(configuration));
|
||||
// stop
|
||||
commandDispatcher.register(new StopCommand());
|
||||
// instance
|
||||
commandDispatcher.register(new InstanceCommand(systemInitializer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,16 +158,10 @@ public final class ManagerLite {
|
||||
/**
|
||||
* 配置初始化
|
||||
*/
|
||||
private static void initConfiguration() {
|
||||
configuration = new PropertiesConfiguration(SystemConstants.USER_SETTINGS_PROPERTIES_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 相关工具初始化
|
||||
*/
|
||||
private static void initTools() {
|
||||
// http 工具类初始化
|
||||
HttpClient.init(configuration);
|
||||
private static void initConfiguration(String[] args) {
|
||||
final CommandLineConfiguration cc = new CommandLineConfiguration(args);
|
||||
cc.from(new PropertiesConfiguration(SystemConstants.USER_SETTINGS_PROPERTIES_PATH));
|
||||
configuration = cc;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.serliunx.ddns.config;
|
||||
|
||||
import com.serliunx.ddns.constant.ConfigurationKeys;
|
||||
import com.serliunx.ddns.support.Assert;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -18,9 +18,28 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
*/
|
||||
public abstract class AbstractConfiguration implements Configuration {
|
||||
|
||||
/**
|
||||
* 日志
|
||||
*/
|
||||
protected final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
/**
|
||||
* 配置值存储
|
||||
*/
|
||||
protected final Map<String, String> valueMap = new LinkedHashMap<>(16);
|
||||
private final Lock loadLock = new ReentrantLock();
|
||||
/**
|
||||
* 上下文更改锁
|
||||
*/
|
||||
protected final Lock contextLock = new ReentrantLock();
|
||||
/**
|
||||
* 监听器
|
||||
* <li> 仅初始化时做增删改操作.
|
||||
*/
|
||||
protected final Map<String, List<ConfigListener>> listeners = new HashMap<>(16);
|
||||
|
||||
/**
|
||||
* 监听所有配置键的监听器标识符
|
||||
*/
|
||||
public static final String ALL_KEYS_LISTENERS_TAG = "ALL_KEYS_LISTENERS_TAG";
|
||||
|
||||
public AbstractConfiguration() {}
|
||||
|
||||
@@ -110,6 +129,77 @@ public abstract class AbstractConfiguration implements Configuration {
|
||||
return valueMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean modify(String key, Object value) {
|
||||
try {
|
||||
contextLock.lock();
|
||||
if (!valueMap.containsKey(key))
|
||||
return false;
|
||||
String oldVal = valueMap.get(key);
|
||||
String newVal = String.valueOf(value);
|
||||
valueMap.put(key, newVal);
|
||||
|
||||
try {
|
||||
invokeListeners(key, oldVal, newVal);
|
||||
} catch (Exception e) {
|
||||
log.warn("监听器执行出现异常 => {}", e.getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
contextLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(String key, Object value, boolean createIfAbsent) {
|
||||
try {
|
||||
contextLock.lock();
|
||||
boolean invoke = false;
|
||||
String oldVal = valueMap.get(key);
|
||||
String newVal = String.valueOf(value);
|
||||
if (!valueMap.containsKey(key)) {
|
||||
if (createIfAbsent) {
|
||||
valueMap.put(key, newVal);
|
||||
invoke = true;
|
||||
}
|
||||
} else {
|
||||
valueMap.put(key, newVal);
|
||||
invoke = true;
|
||||
}
|
||||
|
||||
if (!invoke)
|
||||
return;
|
||||
try {
|
||||
invokeListeners(key, oldVal, newVal);
|
||||
} catch (Exception e) {
|
||||
log.warn("监听器执行出现异常[CIA] => {}", e.getMessage());
|
||||
}
|
||||
} finally {
|
||||
contextLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(ConfigListener listener) {
|
||||
Collection<String> keys = listener.interestedIn();
|
||||
Assert.notNull(keys);
|
||||
if (keys.isEmpty()) {
|
||||
listeners.computeIfAbsent(ALL_KEYS_LISTENERS_TAG, key -> new ArrayList<>())
|
||||
.add(listener);
|
||||
} else {
|
||||
keys.forEach(k -> {
|
||||
listeners.computeIfAbsent(k, k1 -> new ArrayList<>())
|
||||
.add(listener);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<ConfigListener>> getListeners() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return Integer.MAX_VALUE;
|
||||
@@ -120,12 +210,12 @@ public abstract class AbstractConfiguration implements Configuration {
|
||||
*/
|
||||
protected void load() {
|
||||
try {
|
||||
loadLock.lock();
|
||||
contextLock.lock();
|
||||
// 清空原有的配置信息
|
||||
valueMap.clear();
|
||||
load0();
|
||||
}finally {
|
||||
loadLock.unlock();
|
||||
contextLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,4 +239,22 @@ public abstract class AbstractConfiguration implements Configuration {
|
||||
* 载入逻辑
|
||||
*/
|
||||
protected abstract void load0();
|
||||
|
||||
/**
|
||||
* 触发监听器
|
||||
*/
|
||||
private void invokeListeners(String key, Object oldVal, Object newVal) throws Exception {
|
||||
// 触发监听了所有配置项的监听器
|
||||
List<ConfigListener> all = listeners.get(ALL_KEYS_LISTENERS_TAG);
|
||||
for (ConfigListener cl : all) {
|
||||
cl.onChanged(this, key, oldVal, newVal);
|
||||
}
|
||||
// 触发其他监听器
|
||||
List<ConfigListener> listenerList = listeners.get(key);
|
||||
if (listenerList == null || listenerList.isEmpty())
|
||||
return;
|
||||
for (ConfigListener cl : listenerList) {
|
||||
cl.onChanged(this, key, oldVal, newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.serliunx.ddns.config;
|
||||
|
||||
import com.serliunx.ddns.core.Combination;
|
||||
import com.serliunx.ddns.support.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 从启动命令中读取的配置信息
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/12/24
|
||||
*/
|
||||
public final class CommandLineConfiguration extends AbstractConfiguration implements Combination<Configuration> {
|
||||
|
||||
/**
|
||||
* 原始参数
|
||||
*/
|
||||
private final String[] sourceArgs;
|
||||
/**
|
||||
* 待合并的其他配置信息
|
||||
*/
|
||||
private final Collection<Configuration> configurations;
|
||||
|
||||
/**
|
||||
* 配置项标记
|
||||
*/
|
||||
private static final String TAG = "-D";
|
||||
/**
|
||||
* 配置赋值符号(=)
|
||||
*/
|
||||
private static final String EQUAL = "=";
|
||||
/**
|
||||
* 配置缓存
|
||||
*/
|
||||
private final Map<String, String> cache = new HashMap<>();
|
||||
|
||||
public CommandLineConfiguration(String[] sourceArgs) {
|
||||
this(sourceArgs, new ArrayList<>());
|
||||
}
|
||||
|
||||
public CommandLineConfiguration(String[] sourceArgs, Collection<Configuration> configurations) {
|
||||
this.sourceArgs = sourceArgs;
|
||||
this.configurations = configurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始启动参数
|
||||
*
|
||||
* @return 原始启动参数
|
||||
*/
|
||||
public String[] getSourceArgs() {
|
||||
return sourceArgs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void from(Configuration configuration) {
|
||||
configurations.add(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void from(Collection<? extends Configuration> configurations) {
|
||||
this.configurations.addAll(configurations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration getOriginal() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Configuration> getCombinations() {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refresh0() {
|
||||
if (sourceArgs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String arg : sourceArgs) {
|
||||
if (!arg.startsWith(TAG)) {
|
||||
continue;
|
||||
}
|
||||
String key = arg.substring(TAG.length(), arg.indexOf(EQUAL));
|
||||
String value = arg.substring(arg.indexOf(EQUAL) + 1);
|
||||
cache.put(key, value);
|
||||
}
|
||||
|
||||
// 载入配置
|
||||
load();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load0() {
|
||||
// 合并
|
||||
merge();
|
||||
// 更新
|
||||
valueMap.putAll(cache);
|
||||
// 清除缓存
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将其他配置信息合并到当前配置信息
|
||||
* <li> 命令行参数的配置信息优先级高于其他配置信息
|
||||
* <li> 仅在持有锁的情况下访问
|
||||
*/
|
||||
private void merge() {
|
||||
Assert.notEmpty(configurations);
|
||||
|
||||
for (Configuration configuration : configurations) {
|
||||
if (configuration instanceof AbstractConfiguration) {
|
||||
AbstractConfiguration ac = (AbstractConfiguration) configuration;
|
||||
ac.refresh0();
|
||||
}
|
||||
|
||||
final Map<String, String> keyValue = configuration.getAllKeyAndValue();
|
||||
keyValue.forEach((k, v) -> {
|
||||
if (!cache.containsKey(k)) {
|
||||
cache.put(k, v);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/serliunx/ddns/config/ConfigListener.java
Normal file
36
src/main/java/com/serliunx/ddns/config/ConfigListener.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.serliunx.ddns.config;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 配置监听器
|
||||
* <li> 针对配置的变动所需要执行的逻辑
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/3
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ConfigListener {
|
||||
|
||||
/**
|
||||
* 指定当前监听器所感兴趣的配置项(可多个)
|
||||
* <li> 为空时即监听所有配置项
|
||||
*
|
||||
* @return 感兴趣的配置项
|
||||
*/
|
||||
default Collection<String> interestedIn() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置项发生了变动的回调
|
||||
*
|
||||
* @param configuration 配置
|
||||
* @param key 配置键
|
||||
* @param oldVal 旧值
|
||||
* @param newVal 新值
|
||||
*/
|
||||
void onChanged(Configuration configuration, String key, Object oldVal, Object newVal) throws Exception;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.serliunx.ddns.config;
|
||||
import com.serliunx.ddns.core.Priority;
|
||||
import com.serliunx.ddns.core.Refreshable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -100,4 +101,37 @@ public interface Configuration extends Refreshable, Priority {
|
||||
* @return 配置文件所有成功加载的键值对
|
||||
*/
|
||||
Map<String, String> getAllKeyAndValue();
|
||||
|
||||
/**
|
||||
* 修改配置项(锁)
|
||||
*
|
||||
* @param key 配置键
|
||||
* @param value 新的值
|
||||
* @return 成功修改返回真, 否则返回假; 指定键不存在时则修改失败
|
||||
*/
|
||||
boolean modify(String key, Object value);
|
||||
|
||||
/**
|
||||
* 修改配置项(锁)
|
||||
* <li> 指定配置键不存在时则会根绝是否需要创建而新增
|
||||
*
|
||||
* @param key 配置键
|
||||
* @param value 新的值
|
||||
* @param createIfAbsent 是否在不存在指定键时创建
|
||||
*/
|
||||
void modify(String key, Object value, boolean createIfAbsent);
|
||||
|
||||
/**
|
||||
* 添加配置监听器
|
||||
*
|
||||
* @param listener 监听器
|
||||
*/
|
||||
void addListener(ConfigListener listener);
|
||||
|
||||
/**
|
||||
* 获取所有配置监听器
|
||||
*
|
||||
* @return 所有监听器
|
||||
*/
|
||||
Map<String, List<ConfigListener>> getListeners();
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* yml/yaml格式的配置文件,目前用于语言文件
|
||||
* TODO 待重构
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.0
|
||||
* @since 2024/6/17
|
||||
*/
|
||||
@Deprecated
|
||||
public class YamlConfiguration extends FileConfiguration {
|
||||
|
||||
public YamlConfiguration(String path, boolean refresh) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.serliunx.ddns.config.listener;
|
||||
|
||||
import com.serliunx.ddns.config.ConfigListener;
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.constant.ConfigurationKeys;
|
||||
import com.serliunx.ddns.support.Assert;
|
||||
import com.serliunx.ddns.support.ipprovider.ScheduledProvider;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 配置监听器:IP更新间隔变动
|
||||
* <li> 刷新间隔发生变更时通知定时器结束并重新开始计时.
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/3
|
||||
*/
|
||||
public final class IpRefreshIntervalListener implements ConfigListener {
|
||||
|
||||
private final ScheduledProvider scheduledProvider;
|
||||
|
||||
public IpRefreshIntervalListener(ScheduledProvider scheduledProvider) {
|
||||
Assert.notNull(scheduledProvider);
|
||||
this.scheduledProvider = scheduledProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> interestedIn() {
|
||||
return Collections.singletonList(ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(Configuration configuration, String key, Object oldVal, Object newVal) throws Exception {
|
||||
if (key == null
|
||||
|| !key.equals(ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP)
|
||||
|| oldVal == null
|
||||
|| newVal == null
|
||||
|| oldVal.equals(newVal))
|
||||
return;
|
||||
Long newInterval = configuration.getLong(ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP);
|
||||
scheduledProvider.changeTimePeriod(newInterval);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.serliunx.ddns.config.listener;
|
||||
|
||||
import com.serliunx.ddns.config.ConfigListener;
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 配置监听器:通知变更
|
||||
* <li> 仅输出变更信息
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/3
|
||||
*/
|
||||
public final class NotificationConfigListener implements ConfigListener {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NotificationConfigListener.class);
|
||||
|
||||
@Override
|
||||
public void onChanged(Configuration configuration, String key, Object oldVal, Object newVal) throws Exception {
|
||||
if (log.isDebugEnabled())
|
||||
log.debug("配置更新: 配置项 {} 由 {} 调整至 {}", key, oldVal, newVal);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.serliunx.ddns.config;
|
||||
package com.serliunx.ddns.constant;
|
||||
|
||||
/**
|
||||
* 配置文件键常量信息
|
||||
@@ -35,4 +35,9 @@ public final class ConfigurationKeys {
|
||||
* http请求超时时间(秒)
|
||||
*/
|
||||
public static final String KEY_HTTP_OVERTIME = "system.http.overtime";
|
||||
|
||||
/**
|
||||
* ip地址提供器类型
|
||||
*/
|
||||
public static final String KEY_IP_PROVIDER_TYPE = "system.ip.provider.type";
|
||||
}
|
||||
38
src/main/java/com/serliunx/ddns/constant/IpProviderType.java
Normal file
38
src/main/java/com/serliunx/ddns/constant/IpProviderType.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.serliunx.ddns.constant;
|
||||
|
||||
import com.serliunx.ddns.support.ipprovider.IcanhazipProvider;
|
||||
import com.serliunx.ddns.support.ipprovider.IpApiProvider;
|
||||
import com.serliunx.ddns.support.ipprovider.Provider;
|
||||
|
||||
/**
|
||||
* ip供应器类型
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/11/25
|
||||
*/
|
||||
public enum IpProviderType {
|
||||
|
||||
/**
|
||||
* ip数据提供商 <a href="https://ip-api.com/">ip-api</a>
|
||||
* <li> 国外的数据, 国内访问不稳定.
|
||||
*/
|
||||
IP_API(new IpApiProvider()),
|
||||
|
||||
/**
|
||||
* ip数据提供商 <a href="https://icanhazip.com/">icanhazip</a>
|
||||
*/
|
||||
I_CAN_HAZ_IP(new IcanhazipProvider()),
|
||||
|
||||
;
|
||||
|
||||
private final Provider provider;
|
||||
|
||||
IpProviderType(Provider provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public Provider getProvider() {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@@ -82,4 +82,9 @@ public final class SystemConstants {
|
||||
* 用户目录下的.yml配置文件
|
||||
*/
|
||||
public static final String USER_SETTINGS_YAML_PATH = USER_DIR + File.separator + CONFIG_YAML_FILE;
|
||||
|
||||
/**
|
||||
* 程序数据库
|
||||
*/
|
||||
public static final String SQLITE_URL = "jdbc:sqlite:data.db";
|
||||
}
|
||||
|
||||
43
src/main/java/com/serliunx/ddns/core/Combination.java
Normal file
43
src/main/java/com/serliunx/ddns/core/Combination.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.serliunx.ddns.core;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 组合, 将多个对象以一定的逻辑相组合
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/12/24
|
||||
*
|
||||
* @param <E> 合并的类型
|
||||
*/
|
||||
public interface Combination<E> {
|
||||
|
||||
/**
|
||||
* 组合指定对象
|
||||
*
|
||||
* @param e 对象
|
||||
*/
|
||||
void from(E e);
|
||||
|
||||
/**
|
||||
* 批量组合对象
|
||||
*
|
||||
* @param es 对象集合
|
||||
*/
|
||||
void from(Collection<? extends E> es);
|
||||
|
||||
/**
|
||||
* 获取原始对象(未组合前的)
|
||||
*
|
||||
* @return 原始对象
|
||||
*/
|
||||
E getOriginal();
|
||||
|
||||
/**
|
||||
* 获取该对象中所有组合的来源
|
||||
*
|
||||
* @return 所有组合来源
|
||||
*/
|
||||
Collection<? extends E> getCombinations();
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public abstract class AbstractInstanceContext implements InstanceContext, Multip
|
||||
|
||||
@Override
|
||||
public Set<Instance> getInstances() {
|
||||
return instanceMap == null ? Collections.emptySet() : new HashSet<>(instanceMap.values());
|
||||
return instanceMap == null ? Collections.emptySet() : new LinkedHashSet<>(instanceMap.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -43,7 +43,9 @@ public abstract class AbstractInstanceFactory implements InstanceFactory, Listab
|
||||
|
||||
@Override
|
||||
public Map<String, Instance> getInstanceOfType(InstanceType type) {
|
||||
Assert.notNull(instanceMap);
|
||||
if (instanceMap == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return instanceMap.values()
|
||||
.stream()
|
||||
.filter(i -> i.getType().equals(type))
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static com.serliunx.ddns.config.ConfigurationKeys.KEY_ALIYUN_ENDPOINT;
|
||||
import static com.serliunx.ddns.constant.ConfigurationKeys.KEY_ALIYUN_ENDPOINT;
|
||||
import static com.serliunx.ddns.constant.SystemConstants.XML_ROOT_INSTANCE_NAME;
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ public final class Assert {
|
||||
}
|
||||
|
||||
public static void notNull(Object object, String msg) {
|
||||
if(object == null) {
|
||||
if (object == null) {
|
||||
throw new NullPointerException(msg);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,12 @@ public final class Assert {
|
||||
}
|
||||
|
||||
public static void isLargerThan(int source, int target) {
|
||||
if (source <= target) {
|
||||
throw new IllegalArgumentException(String.format("%s太小了, 它必须大于%s", source, target));
|
||||
}
|
||||
}
|
||||
|
||||
public static void isLargerThan(long source, long target) {
|
||||
if(source <= target) {
|
||||
throw new IllegalArgumentException(String.format("%s太小了, 它必须大于%s", source, target));
|
||||
}
|
||||
@@ -43,13 +49,13 @@ public final class Assert {
|
||||
|
||||
public static void notEmpty(Collection<?> collection) {
|
||||
notNull(collection);
|
||||
if(collection.isEmpty())
|
||||
if (collection.isEmpty())
|
||||
throw new IllegalArgumentException("参数不能为空!");
|
||||
}
|
||||
|
||||
public static void notEmpty(CharSequence charSequence) {
|
||||
notNull(charSequence);
|
||||
if(charSequence.length() == 0)
|
||||
if (charSequence.length() == 0)
|
||||
throw new IllegalArgumentException("参数不能为空!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.serliunx.ddns.support;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 控制台样式助手
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
public final class ConsoleStyleHelper {
|
||||
|
||||
/**
|
||||
* ANSI 控制台经典样式编码映射表
|
||||
*/
|
||||
private static final Map<String, String> CLASSIC_STYLE_MAP = new HashMap<>();
|
||||
|
||||
static {
|
||||
// 格式
|
||||
CLASSIC_STYLE_MAP.put("&r", "\033[0m"); // 重置
|
||||
CLASSIC_STYLE_MAP.put("&l", "\033[1m"); // 加粗或高亮
|
||||
|
||||
// 颜色
|
||||
CLASSIC_STYLE_MAP.put("&0", "\033[30m"); // 黑色
|
||||
CLASSIC_STYLE_MAP.put("&1", "\033[31m"); // 红色
|
||||
CLASSIC_STYLE_MAP.put("&2", "\033[32m"); // 绿色
|
||||
CLASSIC_STYLE_MAP.put("&3", "\033[33m"); // 黄色
|
||||
CLASSIC_STYLE_MAP.put("&4", "\033[34m"); // 蓝色
|
||||
CLASSIC_STYLE_MAP.put("&5", "\033[35m"); // 品红
|
||||
CLASSIC_STYLE_MAP.put("&6", "\033[36m"); // 青色
|
||||
CLASSIC_STYLE_MAP.put("&7", "\033[37m"); // 白色
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化输出, 支持颜色代码
|
||||
*
|
||||
* @param format 格式
|
||||
* @param args 参数
|
||||
*/
|
||||
public static void coloredPrintf(String format, final Object... args) {
|
||||
if (!format.endsWith("%n")) {
|
||||
format = format + "%n";
|
||||
}
|
||||
if (!format.endsWith("&r")) {
|
||||
format = format + "&r";
|
||||
}
|
||||
System.out.printf(replaceStyleCode(format), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换样式代码
|
||||
*
|
||||
* @param original 原始文本
|
||||
* @return 替换后的文本
|
||||
*/
|
||||
private static String replaceStyleCode(String original) {
|
||||
Set<Map.Entry<String, String>> entries = CLASSIC_STYLE_MAP.entrySet();
|
||||
for (Map.Entry<String, String> entry : entries) {
|
||||
original = original.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -24,10 +25,28 @@ public final class NetworkContextHolder {
|
||||
// 外网IP地址获取
|
||||
private static final Integer IP_CONTEXT_TIME_OUT = 5;
|
||||
private static volatile String IP_ADDRESS;
|
||||
/**
|
||||
* 失败计数
|
||||
* <p>
|
||||
* 失败次数过多后, 会影响
|
||||
*/
|
||||
private static final AtomicInteger FAILED_COUNTS = new AtomicInteger();
|
||||
|
||||
// private-ctor
|
||||
private NetworkContextHolder() {throw new UnsupportedOperationException();}
|
||||
|
||||
/**
|
||||
* 尝试设置IP地址
|
||||
*
|
||||
* @param i 新的ip地址, 为空时设置会失败
|
||||
*/
|
||||
public static void setIpAddress(String i) {
|
||||
if (i == null
|
||||
|| i.isEmpty()) {
|
||||
log.error("IP 地址不能为空!");
|
||||
FAILED_COUNTS.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
IP_LOCK.lock();
|
||||
IP_ADDRESS = i;
|
||||
@@ -35,11 +54,23 @@ public final class NetworkContextHolder {
|
||||
IP_CONTEXT_WAIT_LATCH.countDown();
|
||||
}
|
||||
} finally {
|
||||
FAILED_COUNTS.set(0);
|
||||
IP_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所缓存的ip的地址
|
||||
* <p>
|
||||
* 设置失败次数过多时将忽略已保存的缓存值,防止多次将旧IP重复更新.
|
||||
*
|
||||
* @return ip地址
|
||||
*/
|
||||
public static String getIpAddress() {
|
||||
if (FAILED_COUNTS.get() > 10) {
|
||||
log.warn("更新失败次数过多, 不在返回IP地址直到下次成功更新!");
|
||||
return null;
|
||||
}
|
||||
if (IP_ADDRESS != null) {
|
||||
return IP_ADDRESS;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package com.serliunx.ddns.support;
|
||||
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.constant.ConfigurationKeys;
|
||||
import com.serliunx.ddns.constant.IpProviderType;
|
||||
import com.serliunx.ddns.constant.SystemConstants;
|
||||
import com.serliunx.ddns.core.Clearable;
|
||||
import com.serliunx.ddns.core.Refreshable;
|
||||
import com.serliunx.ddns.core.context.MultipleSourceInstanceContext;
|
||||
import com.serliunx.ddns.core.instance.Instance;
|
||||
import com.serliunx.ddns.support.okhttp.IPAddressResponse;
|
||||
import com.serliunx.ddns.support.ipprovider.Provider;
|
||||
import com.serliunx.ddns.support.ipprovider.ScheduledProvider;
|
||||
import com.serliunx.ddns.support.okhttp.HttpClient;
|
||||
import com.serliunx.ddns.thread.TaskThreadFactory;
|
||||
import com.serliunx.ddns.support.thread.ThreadFactoryBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -25,8 +28,8 @@ import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.serliunx.ddns.config.ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP;
|
||||
import static com.serliunx.ddns.config.ConfigurationKeys.KEY_THREAD_POOL_CORE_SIZE;
|
||||
import static com.serliunx.ddns.constant.ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP;
|
||||
import static com.serliunx.ddns.constant.ConfigurationKeys.KEY_THREAD_POOL_CORE_SIZE;
|
||||
|
||||
/**
|
||||
* 系统初始化
|
||||
@@ -46,6 +49,7 @@ public final class SystemInitializer implements Refreshable, Clearable {
|
||||
private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
|
||||
private Set<Instance> instances;
|
||||
private final Map<String, ScheduledFuture<?>> runningInstances = new HashMap<>(64);
|
||||
private ScheduledProvider scheduledProvider;
|
||||
|
||||
SystemInitializer(Configuration configuration, MultipleSourceInstanceContext instanceContext, boolean clearCache) {
|
||||
this.configuration = configuration;
|
||||
@@ -76,9 +80,15 @@ public final class SystemInitializer implements Refreshable, Clearable {
|
||||
configuration.refresh();
|
||||
ConfigurationContextHolder.setConfiguration(configuration);
|
||||
|
||||
// 初始化工具类
|
||||
HttpClient.init(configuration);
|
||||
|
||||
// 获取核心线程数量, 默认为CPU核心数量
|
||||
int coreSize = configuration.getInteger(KEY_THREAD_POOL_CORE_SIZE, Runtime.getRuntime().availableProcessors());
|
||||
|
||||
// 初始化ip地址更新任务
|
||||
initIpTask();
|
||||
|
||||
// 初始化线程池
|
||||
initThreadPool(coreSize);
|
||||
|
||||
@@ -109,11 +119,17 @@ public final class SystemInitializer implements Refreshable, Clearable {
|
||||
return instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载实例(不同的容器加载时机不同)
|
||||
*/
|
||||
private void loadInstances() {
|
||||
instances = instanceContext.getInstances();
|
||||
log.info("载入 {} 个实例.", instances.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源释放
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private void releaseResource(String resourceName) {
|
||||
ClassLoader classLoader = SystemConstants.class.getClassLoader();
|
||||
@@ -140,6 +156,9 @@ public final class SystemInitializer implements Refreshable, Clearable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行实例
|
||||
*/
|
||||
private void runInstances() {
|
||||
Assert.notNull(scheduledThreadPoolExecutor);
|
||||
Assert.notNull(instances);
|
||||
@@ -158,37 +177,56 @@ public final class SystemInitializer implements Refreshable, Clearable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化线程池
|
||||
*
|
||||
* @param coreSize 线程池核心线程数量
|
||||
*/
|
||||
private void initThreadPool(int coreSize) {
|
||||
Assert.isLargerThan(coreSize, 1);
|
||||
scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(coreSize, new TaskThreadFactory());
|
||||
scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(coreSize, ThreadFactoryBuilder.builder()
|
||||
.ofNamePattern("ddns-task-%s"));
|
||||
|
||||
// 初始化一个线程保活
|
||||
scheduledThreadPoolExecutor.submit(() -> {});
|
||||
|
||||
// 提交定时获取网络IP的定时任务
|
||||
scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
|
||||
InstanceContextHolder.setAdditional("ip-update");
|
||||
log.info("正在尝试获取本机最新的IP地址.");
|
||||
IPAddressResponse response = HttpClient.getIPAddress();
|
||||
String ip;
|
||||
if(response != null
|
||||
&& (ip = response.getQuery()) != null) {
|
||||
NetworkContextHolder.setIpAddress(ip);
|
||||
log.info("本机最新公网IP地址 => {}", ip);
|
||||
}
|
||||
InstanceContextHolder.clearAdditional();
|
||||
}, 0, configuration.getLong(KEY_TASK_REFRESH_INTERVAL_IP, 300L), TimeUnit.SECONDS);
|
||||
|
||||
// 添加进程结束钩子函数
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
InstanceContextHolder.setAdditional("stopping");
|
||||
log.info("程序正在关闭中, 可能需要一定时间.");
|
||||
scheduledThreadPoolExecutor.shutdown();
|
||||
scheduledProvider.close();
|
||||
log.info("已关闭.");
|
||||
InstanceContextHolder.clearAdditional();
|
||||
}, "DDNS-ShutDownHook"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化定时获取IP地址的任务
|
||||
*/
|
||||
private void initIpTask() {
|
||||
scheduledProvider = new ScheduledProvider(getInternalProvider(),
|
||||
configuration.getLong(KEY_TASK_REFRESH_INTERVAL_IP, 300L));
|
||||
|
||||
scheduledProvider.whenUpdate(ip -> {
|
||||
NetworkContextHolder.setIpAddress(ip);
|
||||
log.debug("本机最新公网IP地址 => {}", ip);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内置的IP供应器(获取IP地址的方式)
|
||||
* <p>
|
||||
* 根据配置文件中的定义{@link ConfigurationKeys#KEY_IP_PROVIDER_TYPE}, 默认为{@link com.serliunx.ddns.support.ipprovider.IpApiProvider}
|
||||
*/
|
||||
private Provider getInternalProvider() {
|
||||
return configuration.getEnum(IpProviderType.class, ConfigurationKeys.KEY_IP_PROVIDER_TYPE,
|
||||
IpProviderType.IP_API).getProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭线程池逻辑
|
||||
*/
|
||||
private void checkAndCloseSafely() {
|
||||
if (scheduledThreadPoolExecutor == null)
|
||||
return;
|
||||
@@ -209,4 +247,11 @@ public final class SystemInitializer implements Refreshable, Clearable {
|
||||
runningInstances.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时IP供应器
|
||||
*/
|
||||
public ScheduledProvider getScheduledProvider() {
|
||||
return scheduledProvider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.serliunx.ddns.support.command;
|
||||
|
||||
import com.serliunx.ddns.ManagerLite;
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.serliunx.ddns.support.ConsoleStyleHelper.coloredPrintf;
|
||||
|
||||
/**
|
||||
* 指令的抽象实现
|
||||
* <li> 实现公共逻辑及定义具体逻辑
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
public abstract class AbstractCommand implements Command {
|
||||
|
||||
private final String name;
|
||||
private final List<Command> subCommands;
|
||||
private final String description;
|
||||
private final String usage;
|
||||
|
||||
protected final Logger log = ManagerLite.getLogger();
|
||||
|
||||
public AbstractCommand(String name, List<Command> subCommands, String description, String usage) {
|
||||
this.name = name;
|
||||
this.subCommands = subCommands;
|
||||
this.description = description;
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Command> getSubCommands() {
|
||||
return subCommands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addSubCommand(Command command) {
|
||||
subCommands.add(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指令逻辑默认实现: 调用子命令
|
||||
*
|
||||
* @param args 当前指令参数
|
||||
* @return 成功执行返回真, 否则返回假. (目前没影响)
|
||||
*/
|
||||
@Override
|
||||
public boolean onCommand(String[] args) {
|
||||
if (!hasArgs(args) ||
|
||||
args.length < 1) {
|
||||
System.out.println();
|
||||
coloredPrintf("&2用法 =>&r &6%s", getUsage());
|
||||
System.out.println();
|
||||
return true;
|
||||
}
|
||||
|
||||
final String subCommand = args[0];
|
||||
List<Command> subCommands = getSubCommands();
|
||||
for (Command command : subCommands) {
|
||||
if (command.getName().equalsIgnoreCase(subCommand)) {
|
||||
return command.onCommand(CommandDispatcher.splitArgs(args));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getArgs() {
|
||||
if (subCommands == null ||
|
||||
subCommands.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return subCommands.stream()
|
||||
.map(Command::getName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(LineReader reader, ParsedLine line, int index, List<Candidate> candidates) {
|
||||
if (index < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String currentWord = line.word();
|
||||
// 补全子命令
|
||||
final List<Command> subCommands = getSubCommands();
|
||||
if (index == 1) {
|
||||
subCommands.forEach(c -> {
|
||||
if (c.getName().startsWith(currentWord)) {
|
||||
candidates.add(new Candidate(c.getName()));
|
||||
}
|
||||
});
|
||||
} else { // 交给子命令补全
|
||||
for (Command c : subCommands) {
|
||||
if (c.getName().equals(line.words().get(1))) {
|
||||
c.onComplete(reader, line, index, candidates);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean hasArgs(String[] args) {
|
||||
return args.length > 0;
|
||||
}
|
||||
}
|
||||
76
src/main/java/com/serliunx/ddns/support/command/Command.java
Normal file
76
src/main/java/com/serliunx/ddns/support/command/Command.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package com.serliunx.ddns.support.command;
|
||||
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.ParsedLine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 指令接口定义
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
public interface Command {
|
||||
|
||||
/**
|
||||
* 指令执行逻辑
|
||||
*
|
||||
* @param args 当前指令参数
|
||||
* @return 成功执行返回真, 否则返回假. (目前没影响)
|
||||
*/
|
||||
boolean onCommand(String[] args);
|
||||
|
||||
/**
|
||||
* 获取指令名称
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* 获取子命令
|
||||
* <li> 例: cmd c1 c2, 此时 c1为cmd的子命令
|
||||
*
|
||||
* @return 子命令
|
||||
*/
|
||||
List<Command> getSubCommands();
|
||||
|
||||
/**
|
||||
* 添加子命令
|
||||
*
|
||||
* @param command 子命令
|
||||
*/
|
||||
void addSubCommand(Command command);
|
||||
|
||||
/**
|
||||
* 获取该指令的描述
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* 获取该指令的用法
|
||||
*/
|
||||
String getUsage();
|
||||
|
||||
/**
|
||||
* 获取参数列表
|
||||
*
|
||||
* @return 参数
|
||||
*/
|
||||
default List<String> getArgs() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令参数补全
|
||||
*
|
||||
* @param reader Jline的LineReader{@link LineReader}
|
||||
* @param line 当前命令行的内容
|
||||
* @param candidates 候选参数列表
|
||||
*/
|
||||
default void onComplete(LineReader reader, ParsedLine line, int index, List<Candidate> candidates) {
|
||||
// do nothing by default.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.serliunx.ddns.support.command;
|
||||
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.Completer;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.ParsedLine;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Jline 命令补全器
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
public class CommandCompleter implements Completer {
|
||||
|
||||
private final CommandDispatcher commandDispatcher;
|
||||
|
||||
public CommandCompleter(CommandDispatcher commandDispatcher) {
|
||||
this.commandDispatcher = commandDispatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
|
||||
final String currentWord = line.word();
|
||||
Map<String, Command> commands = commandDispatcher.getCommands();
|
||||
if (commands == null ||
|
||||
commands.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一个参数补全所有指令
|
||||
if (line.wordIndex() == 0) {
|
||||
commands.keySet().forEach(k -> {
|
||||
if (k.startsWith(currentWord)) {
|
||||
candidates.add(new Candidate(k));
|
||||
}
|
||||
});
|
||||
} else { // 第二个及以后交由具体的指令进行补全逻辑
|
||||
final Command command = commands.get(line.words().get(0));
|
||||
if (command == null) {
|
||||
return;
|
||||
}
|
||||
command.onComplete(reader, line, line.wordIndex(), candidates);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.serliunx.ddns.support.command;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.serliunx.ddns.support.ConsoleStyleHelper.coloredPrintf;
|
||||
|
||||
/**
|
||||
* 指令调度器
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
public final class CommandDispatcher {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CommandDispatcher.class);
|
||||
private static final CommandDispatcher INSTANCE = new CommandDispatcher();
|
||||
|
||||
// private-ctor
|
||||
private CommandDispatcher() {}
|
||||
|
||||
/**
|
||||
* 最顶层指令缓存
|
||||
*/
|
||||
private final Map<String, Command> commands = new LinkedHashMap<>(128);
|
||||
|
||||
/**
|
||||
* 获取所有已注册的指令
|
||||
*
|
||||
* @return 已注册的指令
|
||||
*/
|
||||
public Map<String, Command> getCommands() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指令注册
|
||||
*
|
||||
* @param command 指令
|
||||
*/
|
||||
public synchronized void register(Command command) {
|
||||
commands.put(command.getName(), command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指令反注册
|
||||
*
|
||||
* @param command 指令
|
||||
*/
|
||||
public synchronized void unregister(Command command) {
|
||||
commands.remove(command.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理输入的指令
|
||||
*
|
||||
* @param input 指令
|
||||
*/
|
||||
public void onCommand(String input) {
|
||||
if (input == null ||
|
||||
input.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] args = input.split(" ");
|
||||
String cmd = args[0];
|
||||
|
||||
Command command = commands.get(cmd);
|
||||
if (command == null) {
|
||||
System.out.println();
|
||||
coloredPrintf("&1未知指令&r: &2%s&r, &1请输入 &3help&r &1查看帮助!", cmd);
|
||||
System.out.println();
|
||||
return;
|
||||
}
|
||||
if (!command.onCommand(splitArgs(args))) {
|
||||
coloredPrintf("&1指令执行出现了错误:&r &5%s", Arrays.toString(args));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割指令参数
|
||||
* <li> cmd x1 x2 => x1 x2
|
||||
*
|
||||
* @param args 参数
|
||||
* @return 去除指令本身的参数部分
|
||||
*/
|
||||
public static String[] splitArgs(String[] args) {
|
||||
String[] newArgs = new String[args.length - 1];
|
||||
System.arraycopy(args, 1, newArgs, 0, args.length - 1);
|
||||
return newArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
*/
|
||||
public static CommandDispatcher getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.serliunx.ddns.support.command.target;
|
||||
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
import com.serliunx.ddns.support.command.Command;
|
||||
import com.serliunx.ddns.support.command.CommandDispatcher;
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.ParsedLine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.serliunx.ddns.support.ConsoleStyleHelper.coloredPrintf;
|
||||
|
||||
/**
|
||||
* 指令: help
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
public class HelpCommand extends AbstractCommand {
|
||||
|
||||
public HelpCommand() {
|
||||
super("help", null, "查看帮助信息", "help <指令>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(String[] args) {
|
||||
final Map<String, Command> commands = getAllCommands();
|
||||
|
||||
if (hasArgs(args)) {
|
||||
final String cmd = args[0];
|
||||
final Command command = commands.get(cmd);
|
||||
System.out.println();
|
||||
if (command == null) {
|
||||
coloredPrintf("&1无法找到指令 %s 的相关信息, 请使用 help 查看可用的指令及帮助!%n", cmd);
|
||||
} else {
|
||||
List<Command> subCommands = command.getSubCommands();
|
||||
if (subCommands == null ||
|
||||
subCommands.isEmpty()) {
|
||||
coloredPrintf("&2%s&r - &6%s&r - &5%s%n", cmd, command.getDescription(), command.getUsage());
|
||||
} else {
|
||||
subCommands.forEach(c -> {
|
||||
coloredPrintf("&2%s&r - &6%s&r - &5%s%n", c.getName(), c.getDescription(), c.getUsage());
|
||||
});
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
} else {
|
||||
printCommandDetails(commands);
|
||||
coloredPrintf("&6&l使用 help <指令> 来查看更详细的帮助信息.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getArgs() {
|
||||
final Map<String, Command> commands = getAllCommands();
|
||||
if (commands == null ||
|
||||
commands.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<>(commands.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(LineReader reader, ParsedLine line, int index, List<Candidate> candidates) {
|
||||
final Map<String, Command> commands = getAllCommands();
|
||||
if (commands == null ||
|
||||
commands.isEmpty() || index < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String currentWord = line.word();
|
||||
|
||||
if (index != 1)
|
||||
return;
|
||||
|
||||
commands.keySet().forEach(k -> {
|
||||
if (k.startsWith(currentWord) &&
|
||||
!k.equals("help")) {
|
||||
candidates.add(new Candidate(k));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指令
|
||||
*/
|
||||
private Map<String, Command> getAllCommands() {
|
||||
return CommandDispatcher.getInstance().getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出指令详细信息, 包括子命令及参数信息.
|
||||
*
|
||||
* @param commands 指令集合
|
||||
*/
|
||||
private void printCommandDetails(final Map<String, Command> commands) {
|
||||
if (commands == null || commands.isEmpty())
|
||||
return;
|
||||
System.out.println();
|
||||
System.out.println();
|
||||
commands.forEach((k, v) -> {
|
||||
coloredPrintf("&2%s&r - &6%s&r", k, v.getDescription());
|
||||
coloredPrintf("\t&5用法:&r &3%s", v.getUsage());
|
||||
final List<Command> subCommands = v.getSubCommands();
|
||||
if (subCommands == null || subCommands.isEmpty()) {
|
||||
coloredPrintf("\t&5参数:&r 无");
|
||||
} else {
|
||||
coloredPrintf("\t&5参数:");
|
||||
subCommands.forEach(c -> {
|
||||
coloredPrintf("\t&2%s&r - &6%s&r", c.getName(), c.getDescription());
|
||||
coloredPrintf("\t\t&5用法:&r &3%s", c.getUsage());
|
||||
});
|
||||
}
|
||||
System.out.println();
|
||||
});
|
||||
System.out.println();
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.serliunx.ddns.support.command.target;
|
||||
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.support.SystemInitializer;
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
import com.serliunx.ddns.support.ipprovider.ScheduledProvider;
|
||||
|
||||
import static com.serliunx.ddns.constant.ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP;
|
||||
|
||||
/**
|
||||
* 指令: reload
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/1/16
|
||||
*/
|
||||
public class ReloadCommand extends AbstractCommand {
|
||||
|
||||
/**
|
||||
* 配置信息
|
||||
*/
|
||||
private final Configuration configuration;
|
||||
/**
|
||||
* 系统初始化组件
|
||||
*/
|
||||
private final SystemInitializer systemInitializer;
|
||||
|
||||
public ReloadCommand(Configuration configuration, SystemInitializer systemInitializer) {
|
||||
super("reload", null, "重新载入配置文件.", "reload");
|
||||
this.configuration = configuration;
|
||||
this.systemInitializer = systemInitializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(String[] args) {
|
||||
log.info("正在重新载入配置文件...");
|
||||
if (configuration == null) {
|
||||
return false;
|
||||
}
|
||||
long oldIpInterval = getIpInterval();
|
||||
configuration.refresh();
|
||||
|
||||
// 更新定时查询IP任务
|
||||
triggerScheduledProvider(oldIpInterval);
|
||||
|
||||
log.info("配置文件已重新载入!");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更新周期
|
||||
*/
|
||||
private long getIpInterval() {
|
||||
return configuration.getLong(KEY_TASK_REFRESH_INTERVAL_IP, 300L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时查询IP任务
|
||||
*/
|
||||
private void triggerScheduledProvider(long oldIpInterval) {
|
||||
final ScheduledProvider scheduledProvider = systemInitializer.getScheduledProvider();
|
||||
final long newIpInterval = getIpInterval();
|
||||
if (scheduledProvider != null &&
|
||||
oldIpInterval != newIpInterval) {
|
||||
scheduledProvider.changeTimePeriod(newIpInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.serliunx.ddns.support.command.target;
|
||||
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
|
||||
/**
|
||||
* 指令: stop
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
public final class StopCommand extends AbstractCommand {
|
||||
|
||||
public StopCommand() {
|
||||
super("stop", null, "退出程序", "stop");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(String[] args) {
|
||||
System.exit(0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.serliunx.ddns.support.command.target.config;
|
||||
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 指令: config
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/1/22
|
||||
*/
|
||||
public class ConfigCommand extends AbstractCommand {
|
||||
|
||||
public ConfigCommand(Configuration configuration) {
|
||||
super("config", new ArrayList<>(), "调整配置信息", "config <get/set/...>");
|
||||
// 子命令: set
|
||||
addSubCommand(new ConfigSetCommand(configuration));
|
||||
// 子命令: get
|
||||
addSubCommand(new ConfigGetCommand(configuration));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.serliunx.ddns.support.command.target.config;
|
||||
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import org.jline.reader.Candidate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* config 指令相关工具方法
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
final class ConfigCommandHelper {
|
||||
|
||||
/**
|
||||
* 获取所有配置键, 作为参数返回
|
||||
*
|
||||
* @param configuration 配置信息
|
||||
* @return 配置键集合
|
||||
*/
|
||||
static List<String> getArgs(Configuration configuration) {
|
||||
final Map<String, String> allKeyAndValue;
|
||||
if (configuration == null ||
|
||||
(allKeyAndValue = configuration.getAllKeyAndValue()) == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<>(allKeyAndValue.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全配置键
|
||||
*
|
||||
* @param configuration 配置键
|
||||
* @param currentWord 当前输入内容
|
||||
* @param candidates 候选参数列表
|
||||
*/
|
||||
static void completeConfigKeys(Configuration configuration, String currentWord, List<Candidate> candidates) {
|
||||
final Map<String, String> allKeyAndValue;
|
||||
if (configuration == null ||
|
||||
(allKeyAndValue = configuration.getAllKeyAndValue()) == null) {
|
||||
return;
|
||||
}
|
||||
allKeyAndValue.keySet().forEach(k -> {
|
||||
if (k.startsWith(currentWord)) {
|
||||
candidates.add(new Candidate(k));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.serliunx.ddns.support.command.target.config;
|
||||
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.ParsedLine;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.serliunx.ddns.support.ConsoleStyleHelper.coloredPrintf;
|
||||
|
||||
/**
|
||||
* 指令: config get
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
public final class ConfigGetCommand extends AbstractCommand {
|
||||
|
||||
/**
|
||||
* 配置信息
|
||||
*/
|
||||
private final Configuration configuration;
|
||||
|
||||
public ConfigGetCommand(Configuration configuration) {
|
||||
super("get", null, "获取指定配置项的值", "config get <配置项>");
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(String[] args) {
|
||||
if (!hasArgs(args) ||
|
||||
args.length < 1) {
|
||||
System.out.println();
|
||||
coloredPrintf("&2用法 =>&r &6%s", getUsage());
|
||||
System.out.println();
|
||||
return true;
|
||||
}
|
||||
System.out.println(configuration.getString(args[0]));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getArgs() {
|
||||
return ConfigCommandHelper.getArgs(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(LineReader reader, ParsedLine line, int index, List<Candidate> candidates) {
|
||||
if (index == 2)
|
||||
ConfigCommandHelper.completeConfigKeys(configuration, line.word(), candidates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.serliunx.ddns.support.command.target.config;
|
||||
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.ParsedLine;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.serliunx.ddns.support.ConsoleStyleHelper.coloredPrintf;
|
||||
|
||||
/**
|
||||
* 指令: config set
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
public final class ConfigSetCommand extends AbstractCommand {
|
||||
|
||||
/**
|
||||
* 配置信息
|
||||
*/
|
||||
private final Configuration configuration;
|
||||
|
||||
public ConfigSetCommand(Configuration configuration) {
|
||||
super("set", null, "设置指定配置项的值", "config set <配置项> <新的值>");
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(String[] args) {
|
||||
if (!hasArgs(args) ||
|
||||
args.length < 2) {
|
||||
System.out.println();
|
||||
coloredPrintf("&2用法 =>&r &6%s", getUsage());
|
||||
System.out.println();
|
||||
return true;
|
||||
}
|
||||
final String target = args[0];
|
||||
final String value = args[1];
|
||||
return configuration.modify(target, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getArgs() {
|
||||
return ConfigCommandHelper.getArgs(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(LineReader reader, ParsedLine line, int index, List<Candidate> candidates) {
|
||||
if (index == 2)
|
||||
ConfigCommandHelper.completeConfigKeys(configuration, line.word(), candidates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.serliunx.ddns.support.command.target.instance;
|
||||
|
||||
import com.serliunx.ddns.support.SystemInitializer;
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 指令: instance
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
public final class InstanceCommand extends AbstractCommand {
|
||||
|
||||
public InstanceCommand(SystemInitializer systemInitializer) {
|
||||
super("instance", new ArrayList<>(), "实例相关指令", "instance list/add/...");
|
||||
// 子命令: list
|
||||
addSubCommand(new InstanceListCommand(systemInitializer));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.serliunx.ddns.support.command.target.instance;
|
||||
|
||||
import com.serliunx.ddns.core.instance.Instance;
|
||||
import com.serliunx.ddns.support.Assert;
|
||||
import com.serliunx.ddns.support.ConsoleStyleHelper;
|
||||
import com.serliunx.ddns.support.SystemInitializer;
|
||||
import com.serliunx.ddns.support.command.AbstractCommand;
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.ParsedLine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 指令: instance list
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.4
|
||||
* @since 2025/2/4
|
||||
*/
|
||||
public final class InstanceListCommand extends AbstractCommand {
|
||||
|
||||
private final SystemInitializer systemInitializer;
|
||||
|
||||
public InstanceListCommand(SystemInitializer systemInitializer) {
|
||||
super("list", new ArrayList<>(), "列出所有实例", "instance list");
|
||||
Assert.notNull(systemInitializer);
|
||||
this.systemInitializer = systemInitializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(String[] args) {
|
||||
Set<Instance> instances = systemInitializer.getInstances();
|
||||
System.out.println();
|
||||
|
||||
instances.forEach(i -> {
|
||||
ConsoleStyleHelper.coloredPrintf("&2%s&r(&3%s&r)", i.getName(), i.getType());
|
||||
});
|
||||
|
||||
System.out.println();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getArgs() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(LineReader reader, ParsedLine line, int index, List<Candidate> candidates) {
|
||||
//do nothing for list
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.serliunx.ddns.support.ipprovider;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 抽象的ip提供器, 定义公共逻辑
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/11/25
|
||||
*/
|
||||
public abstract class AbstractProvider implements Provider {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
/**
|
||||
* 运行期间总共的ip查询次数
|
||||
*/
|
||||
protected long total;
|
||||
/**
|
||||
* 上次发生的变动的ip地址
|
||||
*/
|
||||
protected String last = null;
|
||||
/**
|
||||
* 缓存的最新ip地址
|
||||
* <li> !!!该地址为上次获得最新地址, 不一定为当前最新的地址
|
||||
*/
|
||||
protected String cache = null;
|
||||
|
||||
@Override
|
||||
public long getCount() {
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLast() {
|
||||
return last;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
//do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
String ipAddress = doGet();
|
||||
if (ipAddress == null) {
|
||||
log.error("ip地址获取失败!");
|
||||
return null;
|
||||
}
|
||||
total++;
|
||||
|
||||
if (cache == null ||
|
||||
!cache.equals(ipAddress)) {
|
||||
last = cache;
|
||||
cache = ipAddress;
|
||||
}
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取具体的ip地址
|
||||
*/
|
||||
protected abstract String doGet();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.serliunx.ddns.support.ipprovider;
|
||||
|
||||
import com.serliunx.ddns.support.okhttp.HttpClient;
|
||||
|
||||
/**
|
||||
* ip数据提供商 <a href="https://icanhazip.com/">icanhazip</a>
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @since 2024/12/6
|
||||
*/
|
||||
public final class IcanhazipProvider extends AbstractProvider {
|
||||
|
||||
@Override
|
||||
protected String doGet() {
|
||||
return HttpClient.httpGet("https://icanhazip.com/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.serliunx.ddns.support.ipprovider;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.serliunx.ddns.support.okhttp.HttpClient;
|
||||
import com.serliunx.ddns.support.okhttp.IPAddressResponse;
|
||||
|
||||
/**
|
||||
* ip数据提供商 <a href="https://ip-api.com/">ip-api</a>
|
||||
* <li> 国外的数据, 国内访问不稳定.
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/11/25
|
||||
*/
|
||||
public final class IpApiProvider extends AbstractProvider {
|
||||
|
||||
private static final ObjectMapper JSON_MAPPER = new JsonMapper();
|
||||
|
||||
@Override
|
||||
protected String doGet() {
|
||||
final String response = HttpClient.httpGet("http://ip-api.com/json");
|
||||
if (response == null
|
||||
|| response.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
IPAddressResponse ipAddressResponse = JSON_MAPPER.readValue(response, IPAddressResponse.class);
|
||||
|
||||
return ipAddressResponse.getQuery();
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.serliunx.ddns.support.ipprovider;
|
||||
|
||||
/**
|
||||
* ip供应器接口定义
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/11/25
|
||||
*/
|
||||
public interface Provider {
|
||||
|
||||
/**
|
||||
* 获取本次运行期间ip的查询次数
|
||||
*
|
||||
* @return 查询次数
|
||||
*/
|
||||
long getCount();
|
||||
|
||||
/**
|
||||
* 获取上次发生变动的ip地址
|
||||
*
|
||||
* @return 上次发生变动的ip地址
|
||||
*/
|
||||
String getLast();
|
||||
|
||||
/**
|
||||
* 获取最新的ip
|
||||
*
|
||||
* @return 最新的ip
|
||||
*/
|
||||
String get();
|
||||
|
||||
/**
|
||||
* 获取缓存的最新ip地址
|
||||
* <li> !!!该地址为上次获得最新地址, 不一定为当前最新的地址
|
||||
*
|
||||
* @return 缓存的最新ip地址
|
||||
*/
|
||||
String getCache();
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
void init();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.serliunx.ddns.support.ipprovider;
|
||||
|
||||
import com.serliunx.ddns.support.Assert;
|
||||
import com.serliunx.ddns.support.InstanceContextHolder;
|
||||
import com.serliunx.ddns.support.thread.ThreadFactoryBuilder;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 自动更新的ip供应器
|
||||
* <li> 异步更新ip, 获取到的ip地址不一定为最新可用的。
|
||||
* <li> 也可作为简单的任务提交到线程池中执行.
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/11/25
|
||||
*/
|
||||
public class ScheduledProvider extends AbstractProvider implements AutoCloseable, Runnable {
|
||||
|
||||
private final Provider internalProvider;
|
||||
|
||||
/**
|
||||
* 执行周期(秒)
|
||||
*/
|
||||
private volatile long timePeriod;
|
||||
/**
|
||||
* 任务
|
||||
*/
|
||||
private volatile ScheduledFuture<?> task;
|
||||
|
||||
/**
|
||||
* 内置线程池
|
||||
*/
|
||||
private ScheduledThreadPoolExecutor poolExecutor = null;
|
||||
/**
|
||||
* 处理器
|
||||
*/
|
||||
private Consumer<String> valueConsumer = null;
|
||||
/**
|
||||
* 内置缓存
|
||||
*/
|
||||
private volatile String internalCache = null;
|
||||
|
||||
public ScheduledProvider(Provider internalProvider, long timePeriod) {
|
||||
Assert.notNull(internalProvider);
|
||||
Assert.isLargerThan(timePeriod, 0);
|
||||
this.internalProvider = internalProvider;
|
||||
this.timePeriod = timePeriod;
|
||||
init();
|
||||
}
|
||||
|
||||
public ScheduledProvider(Provider internalProvider) {
|
||||
this(internalProvider, 60);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
poolExecutor.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
doAction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
return internalCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
poolExecutor = new ScheduledThreadPoolExecutor(2, ThreadFactoryBuilder.builder()
|
||||
.ofNamePattern("ip-provider-%s")
|
||||
);
|
||||
// 提交
|
||||
submitTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新执行周期
|
||||
* <li> 回替换掉现有的更新任务
|
||||
*
|
||||
* @param timePeriod 新的执行周期
|
||||
*/
|
||||
public void changeTimePeriod(long timePeriod) {
|
||||
Assert.isLargerThan(timePeriod, 0);
|
||||
this.timePeriod = timePeriod;
|
||||
// 取消现有的任务
|
||||
task.cancel(true);
|
||||
submitTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* ip更新时需要执行的逻辑
|
||||
*
|
||||
* @param valueConsumer 逻辑
|
||||
*/
|
||||
public void whenUpdate(Consumer<String> valueConsumer) {
|
||||
this.valueConsumer = valueConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doGet() {
|
||||
// 不应该执行到这里
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交任务逻辑
|
||||
*/
|
||||
private void submitTask() {
|
||||
task = poolExecutor.scheduleAtFixedRate(this, 0, timePeriod, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行逻辑
|
||||
*/
|
||||
private synchronized void doAction() {
|
||||
// 打断时, 终止已有的任务. (逻辑上不应该发生)
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.debug("上一个ip更新任务已终止.");
|
||||
return;
|
||||
}
|
||||
InstanceContextHolder.setAdditional("ip-update");
|
||||
String rawValue = internalProvider.get();
|
||||
if (rawValue == null || rawValue.isEmpty()) {
|
||||
internalCache = null;
|
||||
} else {
|
||||
internalCache = rawValue;
|
||||
}
|
||||
|
||||
if (internalCache != null) {
|
||||
internalCache = internalCache.trim();
|
||||
}
|
||||
|
||||
if (valueConsumer != null) {
|
||||
valueConsumer.accept(internalCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.serliunx.ddns.support.log;
|
||||
|
||||
import ch.qos.logback.classic.PatternLayout;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.AppenderBase;
|
||||
import ch.qos.logback.core.Layout;
|
||||
import org.jline.reader.LineReader;
|
||||
|
||||
/**
|
||||
* 适配JLine的控制台日志输出
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
public final class JLineAdaptAppender extends AppenderBase<ILoggingEvent> {
|
||||
|
||||
/**
|
||||
* JLine的输入读取
|
||||
*/
|
||||
private static LineReader lineReader;
|
||||
|
||||
/**
|
||||
* 格式控制
|
||||
*/
|
||||
private Layout<ILoggingEvent> layout;
|
||||
|
||||
/**
|
||||
* 所配置的格式
|
||||
*/
|
||||
private String pattern;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
super.start();
|
||||
PatternLayout patternLayout = new PatternLayout();
|
||||
patternLayout.setPattern(pattern);
|
||||
patternLayout.setContext(getContext());
|
||||
patternLayout.start();
|
||||
this.layout = patternLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void append(ILoggingEvent event) {
|
||||
if (lineReader != null) {
|
||||
String formattedLog = layout.doLayout(event);
|
||||
lineReader.printAbove(formattedLog);
|
||||
} else {
|
||||
System.out.print(layout.doLayout(event));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置输入读取
|
||||
*
|
||||
* @param lr 读取
|
||||
*/
|
||||
public static void setLineReader(LineReader lr) {
|
||||
lineReader = lr;
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public void setPattern(String pattern) {
|
||||
this.pattern = pattern;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.serliunx.ddns.support.okhttp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.serliunx.ddns.config.Configuration;
|
||||
import com.serliunx.ddns.config.ConfigurationKeys;
|
||||
import com.serliunx.ddns.constant.ConfigurationKeys;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -22,37 +21,44 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
public final class HttpClient {
|
||||
|
||||
private static OkHttpClient CLIENT = null;
|
||||
private static OkHttpClient CLIENT;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HttpClient.class);
|
||||
private static final ObjectMapper JSON_MAPPER = new JsonMapper();
|
||||
private static final int DEFAULT_OVERTIME = 3;
|
||||
|
||||
private HttpClient() {throw new UnsupportedOperationException();}
|
||||
|
||||
static {
|
||||
CLIENT = new OkHttpClient.Builder()
|
||||
.connectTimeout(DEFAULT_OVERTIME, TimeUnit.SECONDS)
|
||||
.readTimeout(DEFAULT_OVERTIME, TimeUnit.SECONDS)
|
||||
.writeTimeout(DEFAULT_OVERTIME, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本机的ip地址
|
||||
* 发送GET请求
|
||||
*
|
||||
* @return 响应结果
|
||||
* @param url 请求地址
|
||||
* @return 响应
|
||||
*/
|
||||
public static IPAddressResponse getIPAddress() {
|
||||
Request request = new Request.Builder()
|
||||
.url("http://ip-api.com/json")
|
||||
public static String httpGet(String url) {
|
||||
final Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = CLIENT.newCall(request).execute()) {
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
if (!response.isSuccessful()
|
||||
|| response.body() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String body = response.body().string();
|
||||
if (body.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final ResponseBody responseBody = response.body();
|
||||
|
||||
return JSON_MAPPER.readValue(body, IPAddressResponse.class);
|
||||
return responseBody.string();
|
||||
} catch (Exception e) {
|
||||
log.error("ip地址获取异常:", e);
|
||||
log.error("http 接口异常:", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -63,7 +69,7 @@ public final class HttpClient {
|
||||
* @param configuration 配置信息
|
||||
*/
|
||||
public static void init(Configuration configuration) {
|
||||
Integer overtime = configuration.getInteger(ConfigurationKeys.KEY_HTTP_OVERTIME, 3);
|
||||
Integer overtime = configuration.getInteger(ConfigurationKeys.KEY_HTTP_OVERTIME, DEFAULT_OVERTIME);
|
||||
|
||||
CLIENT = new OkHttpClient.Builder()
|
||||
.connectTimeout(overtime, TimeUnit.SECONDS)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.serliunx.ddns.support.sqlite;
|
||||
|
||||
import com.serliunx.ddns.constant.SystemConstants;
|
||||
import com.serliunx.ddns.core.Refreshable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* SQLite 数据库连接
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/11/20
|
||||
*/
|
||||
public final class SQLiteConnector implements Refreshable {
|
||||
|
||||
private volatile Connection connection;
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
private final Lock initLock = new ReentrantLock();
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SQLiteConnector.class);
|
||||
private static final SQLiteConnector INSTANCE = new SQLiteConnector();
|
||||
|
||||
// private-ctor
|
||||
private SQLiteConnector() {}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接初始化
|
||||
*/
|
||||
private void init() {
|
||||
if (initialized) {
|
||||
log.warn("sql connection already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initLock.tryLock()) {
|
||||
log.error("sql connection already initialing");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("initialing sqlite connection.");
|
||||
connection = DriverManager.getConnection(SystemConstants.SQLITE_URL);
|
||||
|
||||
// 尝试创建数据库表, 只会执行一次
|
||||
tryCreateTables();
|
||||
|
||||
initialized = true;
|
||||
log.info("sqlite connection successfully initialized.");
|
||||
} catch (Exception e) {
|
||||
initialized = false;
|
||||
log.error("sql connection initialization exception: ", e);
|
||||
} finally {
|
||||
initLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试创建数据库表
|
||||
* <li> 不存在时创建
|
||||
*/
|
||||
private void tryCreateTables() {
|
||||
if (connection == null) {
|
||||
throw new IllegalStateException("sql connection not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已经初始化
|
||||
*/
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
public static SQLiteConnector getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.serliunx.ddns.support.thread;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 线程工厂构建
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/12/3
|
||||
*/
|
||||
public final class ThreadFactoryBuilder {
|
||||
|
||||
/**
|
||||
* 实例
|
||||
*/
|
||||
private static final ThreadFactoryBuilder INSTANCE = new ThreadFactoryBuilder();
|
||||
|
||||
private ThreadFactoryBuilder() {}
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
*/
|
||||
public static ThreadFactoryBuilder builder() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线程工厂之模板名称
|
||||
*
|
||||
* @param pattern 名称模板(如: task-util-%s), %s将根据数量递增
|
||||
* @return 线程工厂
|
||||
*/
|
||||
public ThreadFactory ofNamePattern(final String pattern) {
|
||||
return new NamePatternThreadFactory(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* 线程工厂之模板名称
|
||||
*/
|
||||
private static class NamePatternThreadFactory implements ThreadFactory {
|
||||
|
||||
private final AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private final String pattern;
|
||||
|
||||
public NamePatternThreadFactory(String pattern) {
|
||||
this.pattern = pattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(@NotNull Runnable r) {
|
||||
return new Thread(r, String.format(pattern, counter.getAndIncrement()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.serliunx.ddns.thread;
|
||||
|
||||
import com.serliunx.ddns.support.Assert;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 简易的实例活动相关的线程工厂, 仅仅定义了线程的名称规则.
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.0
|
||||
* @since 2024/5/15
|
||||
*/
|
||||
public class TaskThreadFactory implements ThreadFactory {
|
||||
|
||||
private final AtomicInteger count = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public Thread newThread(@NotNull Runnable r) {
|
||||
Assert.notNull(r);
|
||||
return new Thread(r, String.format(getNamePattern(), count.getAndIncrement()));
|
||||
}
|
||||
|
||||
protected String getNamePattern() {
|
||||
return "ddns-task-%s";
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.serliunx.ddns.thread;
|
||||
|
||||
/**
|
||||
* 同 {@link TaskThreadFactory}, 暂未使用.
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.0
|
||||
* @since 2024/5/15
|
||||
*/
|
||||
public class UtilThreadFactory extends TaskThreadFactory {
|
||||
|
||||
@Override
|
||||
protected String getNamePattern() {
|
||||
return "ddns-util-%s";
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,15 @@
|
||||
<conversionRule conversionWord="instance" converterClass="com.serliunx.ddns.support.log.InstanceNameConverter"/>
|
||||
<conversionRule conversionWord="highlight" converterClass="com.serliunx.ddns.support.log.HighlightingCompositeConverter"/>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>
|
||||
%boldGreen(%d{yyyy-MM-dd HH:mm:ss(SSS)}) %cyan([%pid]) %magenta([%15.15thread]) %green([%16.16instance]) %highlight([%-6level]) %boldCyan(%-36logger{32}): %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
<appender name="JLINE" class="com.serliunx.ddns.support.log.JLineAdaptAppender">
|
||||
<pattern>
|
||||
%boldGreen(%d{yyyy-MM-dd HH:mm:ss(SSS)}) %cyan([%pid]) %magenta([%15.15thread]) %green([%16.16instance]) %highlight([%-6level]) %boldCyan(%-36logger{32}): %msg%n
|
||||
</pattern>
|
||||
</appender>
|
||||
|
||||
<logger name="com.serliunx.ddns" level="DEBUG"/>
|
||||
<logger name="feign" level="DEBUG"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="JLINE" />
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -1,5 +1,6 @@
|
||||
system.cfg.log.onstart=true
|
||||
system.pool.core.size=4
|
||||
system.task.refresh.interval.ip=300
|
||||
system.ip.provider.type=IP_API
|
||||
system.http.overtime=3
|
||||
instance.aliyun.endpoint.url=alidns.cn-hangzhou.aliyuncs.com
|
||||
system.http.overtime=3
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.serliunx.ddns.test;
|
||||
|
||||
import com.serliunx.ddns.constant.SystemConstants;
|
||||
import com.serliunx.ddns.core.Attachment;
|
||||
import com.serliunx.ddns.core.FileAttachment;
|
||||
import com.serliunx.ddns.core.factory.FileInstanceFactory;
|
||||
import com.serliunx.ddns.core.factory.JsonFileInstanceFactory;
|
||||
import com.serliunx.ddns.core.factory.YamlFileInstanceFactory;
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.serliunx.ddns.constant.InstanceType;
|
||||
import com.serliunx.ddns.constant.SystemConstants;
|
||||
import com.serliunx.ddns.core.context.FileInstanceContext;
|
||||
import com.serliunx.ddns.core.context.GenericInstanceContext;
|
||||
import com.serliunx.ddns.core.context.MultipleSourceInstanceContext;
|
||||
import com.serliunx.ddns.core.factory.JsonFileInstanceFactory;
|
||||
import com.serliunx.ddns.core.factory.XmlFileInstanceFactory;
|
||||
import com.serliunx.ddns.core.factory.YamlFileInstanceFactory;
|
||||
@@ -33,13 +32,13 @@ public class ContextTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileContext(){
|
||||
public void testFileContext() {
|
||||
FileInstanceContext context = new FileInstanceContext();
|
||||
context.getSortedListableInstanceFactories().forEach(System.out::println);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyContext(){
|
||||
public void testEmptyContext() {
|
||||
GenericInstanceContext instanceContext = new GenericInstanceContext(false);
|
||||
instanceContext.addListableInstanceFactory(new YamlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR));
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.serliunx.ddns.test;
|
||||
|
||||
import com.serliunx.ddns.constant.InstanceType;
|
||||
import com.serliunx.ddns.constant.SystemConstants;
|
||||
import com.serliunx.ddns.core.factory.InstanceFactory;
|
||||
import com.serliunx.ddns.core.factory.ListableInstanceFactory;
|
||||
import com.serliunx.ddns.core.factory.YamlFileInstanceFactory;
|
||||
import com.serliunx.ddns.core.instance.Instance;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.serliunx.ddns.test.config;
|
||||
|
||||
import com.serliunx.ddns.config.CommandLineConfiguration;
|
||||
import com.serliunx.ddns.config.PropertiesConfiguration;
|
||||
import com.serliunx.ddns.constant.SystemConstants;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 命令行配置读取测试
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @since 2024/12/24
|
||||
*/
|
||||
public class CmdConfigurationTest {
|
||||
|
||||
@Test
|
||||
public void testCmd() {
|
||||
CommandLineConfiguration configuration = new CommandLineConfiguration(new String[]{"-Dtest.env=1",
|
||||
"-Dsystem.cfg.log.onstart=false", "-Dapplication.name=jack"},
|
||||
Collections.singleton(new PropertiesConfiguration(SystemConstants.USER_SETTINGS_PROPERTIES_PATH)));
|
||||
configuration.refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.serliunx.ddns.test.support;
|
||||
|
||||
/**
|
||||
* 供应器测试
|
||||
* //TODO 暂时移除,待重写单元测试
|
||||
*
|
||||
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
|
||||
* @version 1.0.3
|
||||
* @since 2024/11/25
|
||||
*/
|
||||
public class ProviderTest {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user