39 Commits

Author SHA1 Message Date
5d8871eab0 change: 移除数据库的支持 (暂时). 2025-02-06 04:11:52 +08:00
727aa532c6 fix: ip更新失败时任务无法重新开始. 2025-02-05 15:07:03 +08:00
7b74b35794 change: 调整help指令样式. 2025-02-05 09:21:44 +08:00
2cd4eeabd2 feat: 控制台输出样式调整、新增instance指令. 2025-02-04 12:16:43 +08:00
4a05256c59 change: 调整指令样式; 调整config指令. 2025-02-04 10:32:41 +08:00
6f88950359 change: 整理代码. 2025-02-04 09:19:30 +08:00
a8e82b89f8 feat: 新增命令参数补全器; 部分指令调整和适配补全器. 2025-02-04 09:18:25 +08:00
1bb78644d8 change: 重定向jul至slf4j. 2025-02-03 16:04:32 +08:00
fcc8d359c0 feat: 新增配置监听器. 2025-02-03 15:35:52 +08:00
379f5c7032 feat: 新增config指令, 支持热更新配置信息. 2025-01-23 16:13:52 +08:00
afa47c3ff3 fix: 指令调度多线程逻辑异常. 2025-01-20 14:41:56 +08:00
39ac8e7b9f change: 调整reload指令逻辑. 2025-01-18 16:18:54 +08:00
7f727b544e feat: 调整命令逻辑, 新增reload命令. 2025-01-16 15:50:49 +08:00
1bcdabc595 feat: 新增指令调度器. 2025-01-15 21:49:11 +08:00
b3c79f49a0 change: 调整jline检测逻辑, 移除ip-provider相关单元测试. 2025-01-15 15:50:26 +08:00
79216c27bb Merge remote-tracking branch 'origin/master' 2025-01-15 15:23:54 +08:00
7ea0fc57b8 feat: 新增JLine实现更好的控制台输入输出. 2025-01-15 15:23:02 +08:00
ac852b9501 feat: ScheduledProvider现可作为普通任务提交至线程池. 2025-01-12 08:49:38 +08:00
e5061c1233 change: 关闭逻辑调整. 2025-01-07 15:02:24 +08:00
c93309a7d2 fix: ip值去除多余空格、换行符等. 2025-01-07 14:43:54 +08:00
4b974528b1 change: 将工具类初始化时机推迟到系统初始化. 2025-01-07 10:32:43 +08:00
922135c3a7 Revert "feat: 新增全局代理配置."
This reverts commit b3ca9c2647.
2025-01-07 10:29:44 +08:00
42774eed50 change: 代码整理. 2025-01-07 10:19:50 +08:00
b3ca9c2647 feat: 新增全局代理配置. 2025-01-07 10:18:28 +08:00
05b637b9f1 change: 代码注释、调整代码. 2024-12-25 21:48:01 +08:00
c2b71736cc change: ip地址获取失败时, 阻塞实例的更新任务. 2024-12-25 14:43:51 +08:00
a0b0764ac3 feat: 新增命令行配置、优先级最高. 2024-12-25 14:28:30 +08:00
aa934121dc fix: ip获取异常导致的实例运行异常. 2024-12-07 07:40:59 +08:00
048c358800 feat: 新增icanhazip IP地址查询、调整HttpClient职能. 2024-12-06 15:50:45 +08:00
c7ea252159 fix: 定时ip更新频率过高时失效. 2024-12-06 08:54:11 +08:00
ac81e48d9c Merge remote-tracking branch 'origin/master' 2024-12-03 16:42:24 +08:00
9900ccf1f5 feat: 线程工厂模板. 2024-12-03 16:41:35 +08:00
f66390e86b change: 调整测试用例. 2024-12-01 10:37:13 +08:00
a729a5d99c change: 废弃yaml配置文件, 待重构. 2024-11-26 11:17:18 +08:00
SerLiunx-ctrl
d0e1792e4b Merge pull request #1 from SerLiunx-ctrl/ip_provider
feat: 解耦ip更新源.
2024-11-25 16:04:07 +08:00
fe86653903 feat: 解耦ip更新源. 2024-11-25 15:37:07 +08:00
eefd907866 feat: 初始化SQLite连接. 2024-11-20 13:08:38 +08:00
f70db6ef90 fix: 首次根据类型获取示例异常. 2024-11-20 10:50:38 +08:00
d929975809 feat: 新增数据库支持. 2024-11-20 10:43:24 +08:00
51 changed files with 2231 additions and 127 deletions

23
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>com.serliunx.ddns</groupId> <groupId>com.serliunx.ddns</groupId>
<artifactId>ddns-manager-lite</artifactId> <artifactId>ddns-manager-lite</artifactId>
<version>1.0.2</version> <version>1.0.3-alpha</version>
<properties> <properties>
<maven.compiler.source>8</maven.compiler.source> <maven.compiler.source>8</maven.compiler.source>
@@ -19,6 +19,8 @@
<snakeyaml.version>1.30</snakeyaml.version> <snakeyaml.version>1.30</snakeyaml.version>
<aliyundns.sdk.version>3.0.14</aliyundns.sdk.version> <aliyundns.sdk.version>3.0.14</aliyundns.sdk.version>
<tencent.dnspod.sdk.version>3.1.1002</tencent.dnspod.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> </properties>
<dependencies> <dependencies>
@@ -55,9 +57,26 @@
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<version>4.13.2</version> <version>${junit.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </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> </dependencies>
<build> <build>

View File

@@ -1,12 +1,33 @@
package com.serliunx.ddns; package com.serliunx.ddns;
import com.serliunx.ddns.config.CommandLineConfiguration;
import com.serliunx.ddns.config.Configuration; import com.serliunx.ddns.config.Configuration;
import com.serliunx.ddns.config.PropertiesConfiguration; 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.constant.SystemConstants;
import com.serliunx.ddns.core.context.FileInstanceContext; import com.serliunx.ddns.core.context.FileInstanceContext;
import com.serliunx.ddns.core.context.MultipleSourceInstanceContext; import com.serliunx.ddns.core.context.MultipleSourceInstanceContext;
import com.serliunx.ddns.support.InstanceContextHolder;
import com.serliunx.ddns.support.SystemInitializer; 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 { 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 SystemInitializer systemInitializer;
/**
* 指令调度
*/
private static CommandDispatcher commandDispatcher;
/**
* 获取默认的日志输出
*/
public static Logger getLogger() {
return DEFAULT_LOGGER;
}
public static void main(String[] args) { public static void main(String[] args) {
// 初始化slf4j日志桥接
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
// 配置初始化 // 配置初始化
initConfiguration(); initConfiguration(args);
// 相关工具初始化
initTools();
// 初始化实例容器 // 初始化实例容器
initContext(); initContext();
// 系统初始化 // 系统初始化
initSystem(); 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() { private static void initConfiguration(String[] args) {
configuration = new PropertiesConfiguration(SystemConstants.USER_SETTINGS_PROPERTIES_PATH); final CommandLineConfiguration cc = new CommandLineConfiguration(args);
} cc.from(new PropertiesConfiguration(SystemConstants.USER_SETTINGS_PROPERTIES_PATH));
configuration = cc;
/**
* 相关工具初始化
*/
private static void initTools() {
// http 工具类初始化
HttpClient.init(configuration);
} }
/** /**

View File

@@ -1,11 +1,11 @@
package com.serliunx.ddns.config; package com.serliunx.ddns.config;
import com.serliunx.ddns.constant.ConfigurationKeys;
import com.serliunx.ddns.support.Assert; import com.serliunx.ddns.support.Assert;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap; import java.util.*;
import java.util.Map;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -18,9 +18,28 @@ import java.util.concurrent.locks.ReentrantLock;
*/ */
public abstract class AbstractConfiguration implements Configuration { public abstract class AbstractConfiguration implements Configuration {
/**
* 日志
*/
protected final Logger log = LoggerFactory.getLogger(this.getClass()); protected final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 配置值存储
*/
protected final Map<String, String> valueMap = new LinkedHashMap<>(16); 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() {} public AbstractConfiguration() {}
@@ -110,6 +129,77 @@ public abstract class AbstractConfiguration implements Configuration {
return valueMap; 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 @Override
public int getPriority() { public int getPriority() {
return Integer.MAX_VALUE; return Integer.MAX_VALUE;
@@ -120,12 +210,12 @@ public abstract class AbstractConfiguration implements Configuration {
*/ */
protected void load() { protected void load() {
try { try {
loadLock.lock(); contextLock.lock();
// 清空原有的配置信息 // 清空原有的配置信息
valueMap.clear(); valueMap.clear();
load0(); load0();
}finally { }finally {
loadLock.unlock(); contextLock.unlock();
} }
} }
@@ -149,4 +239,22 @@ public abstract class AbstractConfiguration implements Configuration {
* 载入逻辑 * 载入逻辑
*/ */
protected abstract void load0(); 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);
}
}
} }

View File

@@ -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);
}
});
}
}
}

View 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;
}

View File

@@ -3,6 +3,7 @@ package com.serliunx.ddns.config;
import com.serliunx.ddns.core.Priority; import com.serliunx.ddns.core.Priority;
import com.serliunx.ddns.core.Refreshable; import com.serliunx.ddns.core.Refreshable;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@@ -100,4 +101,37 @@ public interface Configuration extends Refreshable, Priority {
* @return 配置文件所有成功加载的键值对 * @return 配置文件所有成功加载的键值对
*/ */
Map<String, String> getAllKeyAndValue(); 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();
} }

View File

@@ -9,11 +9,13 @@ import java.util.Map;
/** /**
* yml/yaml格式的配置文件目前用于语言文件 * yml/yaml格式的配置文件目前用于语言文件
* TODO 待重构
* *
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a> * @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
* @version 1.0.0 * @version 1.0.0
* @since 2024/6/17 * @since 2024/6/17
*/ */
@Deprecated
public class YamlConfiguration extends FileConfiguration { public class YamlConfiguration extends FileConfiguration {
public YamlConfiguration(String path, boolean refresh) { public YamlConfiguration(String path, boolean refresh) {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,4 +1,4 @@
package com.serliunx.ddns.config; package com.serliunx.ddns.constant;
/** /**
* 配置文件键常量信息 * 配置文件键常量信息
@@ -35,4 +35,9 @@ public final class ConfigurationKeys {
* http请求超时时间() * http请求超时时间()
*/ */
public static final String KEY_HTTP_OVERTIME = "system.http.overtime"; public static final String KEY_HTTP_OVERTIME = "system.http.overtime";
/**
* ip地址提供器类型
*/
public static final String KEY_IP_PROVIDER_TYPE = "system.ip.provider.type";
} }

View 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;
}
}

View File

@@ -82,4 +82,9 @@ public final class SystemConstants {
* 用户目录下的.yml配置文件 * 用户目录下的.yml配置文件
*/ */
public static final String USER_SETTINGS_YAML_PATH = USER_DIR + File.separator + CONFIG_YAML_FILE; 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";
} }

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

View File

@@ -109,7 +109,7 @@ public abstract class AbstractInstanceContext implements InstanceContext, Multip
@Override @Override
public Set<Instance> getInstances() { public Set<Instance> getInstances() {
return instanceMap == null ? Collections.emptySet() : new HashSet<>(instanceMap.values()); return instanceMap == null ? Collections.emptySet() : new LinkedHashSet<>(instanceMap.values());
} }
@Override @Override

View File

@@ -43,7 +43,9 @@ public abstract class AbstractInstanceFactory implements InstanceFactory, Listab
@Override @Override
public Map<String, Instance> getInstanceOfType(InstanceType type) { public Map<String, Instance> getInstanceOfType(InstanceType type) {
Assert.notNull(instanceMap); if (instanceMap == null) {
return Collections.emptyMap();
}
return instanceMap.values() return instanceMap.values()
.stream() .stream()
.filter(i -> i.getType().equals(type)) .filter(i -> i.getType().equals(type))

View File

@@ -16,7 +16,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; 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; import static com.serliunx.ddns.constant.SystemConstants.XML_ROOT_INSTANCE_NAME;
/** /**

View File

@@ -41,6 +41,12 @@ public final class Assert {
} }
} }
public static void isLargerThan(long source, long target) {
if(source <= target) {
throw new IllegalArgumentException(String.format("%s太小了, 它必须大于%s", source, target));
}
}
public static void notEmpty(Collection<?> collection) { public static void notEmpty(Collection<?> collection) {
notNull(collection); notNull(collection);
if (collection.isEmpty()) if (collection.isEmpty())

View File

@@ -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;
}
}

View File

@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -24,10 +25,28 @@ public final class NetworkContextHolder {
// 外网IP地址获取 // 外网IP地址获取
private static final Integer IP_CONTEXT_TIME_OUT = 5; private static final Integer IP_CONTEXT_TIME_OUT = 5;
private static volatile String IP_ADDRESS; private static volatile String IP_ADDRESS;
/**
* 失败计数
* <p>
* 失败次数过多后, 会影响
*/
private static final AtomicInteger FAILED_COUNTS = new AtomicInteger();
// private-ctor
private NetworkContextHolder() {throw new UnsupportedOperationException();} private NetworkContextHolder() {throw new UnsupportedOperationException();}
/**
* 尝试设置IP地址
*
* @param i 新的ip地址, 为空时设置会失败
*/
public static void setIpAddress(String i) { public static void setIpAddress(String i) {
if (i == null
|| i.isEmpty()) {
log.error("IP 地址不能为空!");
FAILED_COUNTS.incrementAndGet();
return;
}
try { try {
IP_LOCK.lock(); IP_LOCK.lock();
IP_ADDRESS = i; IP_ADDRESS = i;
@@ -35,11 +54,23 @@ public final class NetworkContextHolder {
IP_CONTEXT_WAIT_LATCH.countDown(); IP_CONTEXT_WAIT_LATCH.countDown();
} }
} finally { } finally {
FAILED_COUNTS.set(0);
IP_LOCK.unlock(); IP_LOCK.unlock();
} }
} }
/**
* 获取所缓存的ip的地址
* <p>
* 设置失败次数过多时将忽略已保存的缓存值防止多次将旧IP重复更新.
*
* @return ip地址
*/
public static String getIpAddress() { public static String getIpAddress() {
if (FAILED_COUNTS.get() > 10) {
log.warn("更新失败次数过多, 不在返回IP地址直到下次成功更新!");
return null;
}
if (IP_ADDRESS != null) { if (IP_ADDRESS != null) {
return IP_ADDRESS; return IP_ADDRESS;
} }

View File

@@ -1,14 +1,17 @@
package com.serliunx.ddns.support; package com.serliunx.ddns.support;
import com.serliunx.ddns.config.Configuration; 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.constant.SystemConstants;
import com.serliunx.ddns.core.Clearable; import com.serliunx.ddns.core.Clearable;
import com.serliunx.ddns.core.Refreshable; import com.serliunx.ddns.core.Refreshable;
import com.serliunx.ddns.core.context.MultipleSourceInstanceContext; import com.serliunx.ddns.core.context.MultipleSourceInstanceContext;
import com.serliunx.ddns.core.instance.Instance; 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.support.okhttp.HttpClient;
import com.serliunx.ddns.thread.TaskThreadFactory; import com.serliunx.ddns.support.thread.ThreadFactoryBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -25,8 +28,8 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static com.serliunx.ddns.config.ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP; import static com.serliunx.ddns.constant.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_THREAD_POOL_CORE_SIZE;
/** /**
* 系统初始化 * 系统初始化
@@ -46,6 +49,7 @@ public final class SystemInitializer implements Refreshable, Clearable {
private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor; private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
private Set<Instance> instances; private Set<Instance> instances;
private final Map<String, ScheduledFuture<?>> runningInstances = new HashMap<>(64); private final Map<String, ScheduledFuture<?>> runningInstances = new HashMap<>(64);
private ScheduledProvider scheduledProvider;
SystemInitializer(Configuration configuration, MultipleSourceInstanceContext instanceContext, boolean clearCache) { SystemInitializer(Configuration configuration, MultipleSourceInstanceContext instanceContext, boolean clearCache) {
this.configuration = configuration; this.configuration = configuration;
@@ -76,9 +80,15 @@ public final class SystemInitializer implements Refreshable, Clearable {
configuration.refresh(); configuration.refresh();
ConfigurationContextHolder.setConfiguration(configuration); ConfigurationContextHolder.setConfiguration(configuration);
// 初始化工具类
HttpClient.init(configuration);
// 获取核心线程数量, 默认为CPU核心数量 // 获取核心线程数量, 默认为CPU核心数量
int coreSize = configuration.getInteger(KEY_THREAD_POOL_CORE_SIZE, Runtime.getRuntime().availableProcessors()); int coreSize = configuration.getInteger(KEY_THREAD_POOL_CORE_SIZE, Runtime.getRuntime().availableProcessors());
// 初始化ip地址更新任务
initIpTask();
// 初始化线程池 // 初始化线程池
initThreadPool(coreSize); initThreadPool(coreSize);
@@ -109,11 +119,17 @@ public final class SystemInitializer implements Refreshable, Clearable {
return instances; return instances;
} }
/**
* 加载实例(不同的容器加载时机不同)
*/
private void loadInstances() { private void loadInstances() {
instances = instanceContext.getInstances(); instances = instanceContext.getInstances();
log.info("载入 {} 个实例.", instances.size()); log.info("载入 {} 个实例.", instances.size());
} }
/**
* 资源释放
*/
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
private void releaseResource(String resourceName) { private void releaseResource(String resourceName) {
ClassLoader classLoader = SystemConstants.class.getClassLoader(); ClassLoader classLoader = SystemConstants.class.getClassLoader();
@@ -140,6 +156,9 @@ public final class SystemInitializer implements Refreshable, Clearable {
} }
} }
/**
* 运行实例
*/
private void runInstances() { private void runInstances() {
Assert.notNull(scheduledThreadPoolExecutor); Assert.notNull(scheduledThreadPoolExecutor);
Assert.notNull(instances); Assert.notNull(instances);
@@ -158,37 +177,56 @@ public final class SystemInitializer implements Refreshable, Clearable {
} }
} }
/**
* 初始化线程池
*
* @param coreSize 线程池核心线程数量
*/
private void initThreadPool(int coreSize) { private void initThreadPool(int coreSize) {
Assert.isLargerThan(coreSize, 1); Assert.isLargerThan(coreSize, 1);
scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(coreSize, new TaskThreadFactory()); scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(coreSize, ThreadFactoryBuilder.builder()
.ofNamePattern("ddns-task-%s"));
// 初始化一个线程保活 // 初始化一个线程保活
scheduledThreadPoolExecutor.submit(() -> {}); 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(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
InstanceContextHolder.setAdditional("stopping"); InstanceContextHolder.setAdditional("stopping");
log.info("程序正在关闭中, 可能需要一定时间."); log.info("程序正在关闭中, 可能需要一定时间.");
scheduledThreadPoolExecutor.shutdown(); scheduledThreadPoolExecutor.shutdown();
scheduledProvider.close();
log.info("已关闭."); log.info("已关闭.");
InstanceContextHolder.clearAdditional(); InstanceContextHolder.clearAdditional();
}, "DDNS-ShutDownHook")); }, "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() { private void checkAndCloseSafely() {
if (scheduledThreadPoolExecutor == null) if (scheduledThreadPoolExecutor == null)
return; return;
@@ -209,4 +247,11 @@ public final class SystemInitializer implements Refreshable, Clearable {
runningInstances.clear(); runningInstances.clear();
} }
} }
/**
* 获取定时IP供应器
*/
public ScheduledProvider getScheduledProvider() {
return scheduledProvider;
}
} }

View File

@@ -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;
}
}

View 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.
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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
}
}

View File

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

View File

@@ -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/");
}
}

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,12 +1,11 @@
package com.serliunx.ddns.support.okhttp; 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.Configuration;
import com.serliunx.ddns.config.ConfigurationKeys; import com.serliunx.ddns.constant.ConfigurationKeys;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -22,37 +21,44 @@ import java.util.concurrent.TimeUnit;
*/ */
public final class HttpClient { 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 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();} 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() { public static String httpGet(String url) {
Request request = new Request.Builder() final Request request = new Request.Builder()
.url("http://ip-api.com/json") .url(url)
.get() .get()
.build(); .build();
try (Response response = CLIENT.newCall(request).execute()) { try (Response response = CLIENT.newCall(request).execute()) {
if (!response.isSuccessful() || response.body() == null) { if (!response.isSuccessful()
|| response.body() == null) {
return null; return null;
} }
String body = response.body().string(); final ResponseBody responseBody = response.body();
if (body.isEmpty()) {
return null;
}
return JSON_MAPPER.readValue(body, IPAddressResponse.class); return responseBody.string();
} catch (Exception e) { } catch (Exception e) {
log.error("ip地址获取异常:", e); log.error("http 接口异常:", e);
} }
return null; return null;
} }
@@ -63,7 +69,7 @@ public final class HttpClient {
* @param configuration 配置信息 * @param configuration 配置信息
*/ */
public static void init(Configuration 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() CLIENT = new OkHttpClient.Builder()
.connectTimeout(overtime, TimeUnit.SECONDS) .connectTimeout(overtime, TimeUnit.SECONDS)

View File

@@ -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;
}
}

View File

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

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View File

@@ -3,18 +3,15 @@
<conversionRule conversionWord="instance" converterClass="com.serliunx.ddns.support.log.InstanceNameConverter"/> <conversionRule conversionWord="instance" converterClass="com.serliunx.ddns.support.log.InstanceNameConverter"/>
<conversionRule conversionWord="highlight" converterClass="com.serliunx.ddns.support.log.HighlightingCompositeConverter"/> <conversionRule conversionWord="highlight" converterClass="com.serliunx.ddns.support.log.HighlightingCompositeConverter"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="JLINE" class="com.serliunx.ddns.support.log.JLineAdaptAppender">
<encoder>
<pattern> <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 %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> </pattern>
</encoder>
</appender> </appender>
<logger name="com.serliunx.ddns" level="DEBUG"/> <logger name="com.serliunx.ddns" level="DEBUG"/>
<logger name="feign" level="DEBUG"/>
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT" /> <appender-ref ref="JLINE" />
</root> </root>
</configuration> </configuration>

View File

@@ -1,5 +1,6 @@
system.cfg.log.onstart=true system.cfg.log.onstart=true
system.pool.core.size=4 system.pool.core.size=4
system.task.refresh.interval.ip=300 system.task.refresh.interval.ip=300
instance.aliyun.endpoint.url=alidns.cn-hangzhou.aliyuncs.com system.ip.provider.type=IP_API
system.http.overtime=3 system.http.overtime=3
instance.aliyun.endpoint.url=alidns.cn-hangzhou.aliyuncs.com

View File

@@ -1,10 +1,7 @@
package com.serliunx.ddns.test; package com.serliunx.ddns.test;
import com.serliunx.ddns.constant.SystemConstants; 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.FileInstanceFactory;
import com.serliunx.ddns.core.factory.JsonFileInstanceFactory;
import com.serliunx.ddns.core.factory.YamlFileInstanceFactory; import com.serliunx.ddns.core.factory.YamlFileInstanceFactory;
import org.junit.Test; import org.junit.Test;

View File

@@ -4,7 +4,6 @@ import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.constant.SystemConstants; import com.serliunx.ddns.constant.SystemConstants;
import com.serliunx.ddns.core.context.FileInstanceContext; import com.serliunx.ddns.core.context.FileInstanceContext;
import com.serliunx.ddns.core.context.GenericInstanceContext; 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.JsonFileInstanceFactory;
import com.serliunx.ddns.core.factory.XmlFileInstanceFactory; import com.serliunx.ddns.core.factory.XmlFileInstanceFactory;
import com.serliunx.ddns.core.factory.YamlFileInstanceFactory; import com.serliunx.ddns.core.factory.YamlFileInstanceFactory;

View File

@@ -2,7 +2,6 @@ package com.serliunx.ddns.test;
import com.serliunx.ddns.constant.InstanceType; import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.constant.SystemConstants; 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.ListableInstanceFactory;
import com.serliunx.ddns.core.factory.YamlFileInstanceFactory; import com.serliunx.ddns.core.factory.YamlFileInstanceFactory;
import com.serliunx.ddns.core.instance.Instance; import com.serliunx.ddns.core.instance.Instance;

View File

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

View File

@@ -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 {
}