33 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
46 changed files with 1809 additions and 212 deletions

11
pom.xml
View File

@@ -66,6 +66,17 @@
<artifactId>sqlite-jdbc</artifactId> <artifactId>sqlite-jdbc</artifactId>
<version>${sqlite.jdbc.version}</version> <version>${sqlite.jdbc.version}</version>
</dependency> </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

@@ -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;
/** /**
* 配置文件键常量信息 * 配置文件键常量信息

View File

@@ -1,5 +1,6 @@
package com.serliunx.ddns.constant; 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.IpApiProvider;
import com.serliunx.ddns.support.ipprovider.Provider; import com.serliunx.ddns.support.ipprovider.Provider;
@@ -18,6 +19,11 @@ public enum IpProviderType {
*/ */
IP_API(new IpApiProvider()), IP_API(new IpApiProvider()),
/**
* ip数据提供商 <a href="https://icanhazip.com/">icanhazip</a>
*/
I_CAN_HAZ_IP(new IcanhazipProvider()),
; ;
private final Provider provider; private final Provider provider;

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

@@ -1,36 +0,0 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.support.Assert;
import com.serliunx.ddns.support.sqlite.SQLiteConnector;
import java.util.Collections;
import java.util.Set;
/**
* 数据库示例工厂: 从数据库中(sqlite)存储、加载示例信息
*
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
* @version 1.0.3
* @since 2024/11/20
*/
public final class DatabaseInstanceFactory extends AbstractInstanceFactory implements PersistentInstanceFactory {
private final SQLiteConnector connector;
public DatabaseInstanceFactory(SQLiteConnector connector) {
this.connector = connector;
}
@Override
protected Set<Instance> load() {
Assert.notNull(connector, "数据库连接不能为空!");
connector.refresh();
return Collections.emptySet();
}
@Override
public boolean save(Instance instance) {
return false;
}
}

View File

@@ -1,21 +0,0 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.core.instance.Instance;
/**
* 可持久化的实例工厂, 支持编辑、保存实例数据.
*
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
* @version 1.0.3
* @since 2024/11/20
*/
public interface PersistentInstanceFactory extends InstanceFactory {
/**
* 保存实例信息
*
* @param instance 实例
* @return 成功保存返回真, 否则返回假.
*/
boolean save(Instance instance);
}

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

@@ -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,19 +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.config.ConfigurationKeys; import com.serliunx.ddns.constant.ConfigurationKeys;
import com.serliunx.ddns.constant.IpProviderType; 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.ipprovider.IpApiProvider;
import com.serliunx.ddns.support.ipprovider.Provider; import com.serliunx.ddns.support.ipprovider.Provider;
import com.serliunx.ddns.support.ipprovider.ScheduledProvider; import com.serliunx.ddns.support.ipprovider.ScheduledProvider;
import com.serliunx.ddns.support.okhttp.IPAddressResponse;
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;
@@ -30,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;
/** /**
* 系统初始化 * 系统初始化
@@ -82,6 +80,9 @@ 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());
@@ -118,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();
@@ -149,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);
@@ -167,9 +177,15 @@ 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(() -> {});
@@ -179,26 +195,38 @@ public final class SystemInitializer implements Refreshable, Clearable {
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() { private void initIpTask() {
scheduledProvider = new ScheduledProvider(getInternalProvider(), scheduledProvider = new ScheduledProvider(getInternalProvider(),
configuration.getLong(KEY_TASK_REFRESH_INTERVAL_IP, 300L)); configuration.getLong(KEY_TASK_REFRESH_INTERVAL_IP, 300L));
scheduledProvider.whenUpdate(ip -> { scheduledProvider.whenUpdate(ip -> {
NetworkContextHolder.setIpAddress(ip); NetworkContextHolder.setIpAddress(ip);
log.info("本机最新公网IP地址 => {}", ip); log.debug("本机最新公网IP地址 => {}", ip);
}); });
} }
/**
* 获取内置的IP供应器获取IP地址的方式
* <p>
* 根据配置文件中的定义{@link ConfigurationKeys#KEY_IP_PROVIDER_TYPE}, 默认为{@link com.serliunx.ddns.support.ipprovider.IpApiProvider}
*/
private Provider getInternalProvider() { private Provider getInternalProvider() {
return configuration.getEnum(IpProviderType.class, ConfigurationKeys.KEY_IP_PROVIDER_TYPE, return configuration.getEnum(IpProviderType.class, ConfigurationKeys.KEY_IP_PROVIDER_TYPE,
IpProviderType.IP_API).getProvider(); IpProviderType.IP_API).getProvider();
} }
/**
* 关闭线程池逻辑
*/
private void checkAndCloseSafely() { private void checkAndCloseSafely() {
if (scheduledThreadPoolExecutor == null) if (scheduledThreadPoolExecutor == null)
return; return;
@@ -219,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,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

@@ -1,5 +1,8 @@
package com.serliunx.ddns.support.ipprovider; 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.HttpClient;
import com.serliunx.ddns.support.okhttp.IPAddressResponse; import com.serliunx.ddns.support.okhttp.IPAddressResponse;
@@ -13,12 +16,22 @@ import com.serliunx.ddns.support.okhttp.IPAddressResponse;
*/ */
public final class IpApiProvider extends AbstractProvider { public final class IpApiProvider extends AbstractProvider {
private static final ObjectMapper JSON_MAPPER = new JsonMapper();
@Override @Override
protected String doGet() { protected String doGet() {
IPAddressResponse response = HttpClient.getIPAddress(); final String response = HttpClient.httpGet("http://ip-api.com/json");
if (response == null) { if (response == null
|| response.isEmpty()) {
return null; return null;
} }
return response.getQuery();
try {
IPAddressResponse ipAddressResponse = JSON_MAPPER.readValue(response, IPAddressResponse.class);
return ipAddressResponse.getQuery();
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
} }
} }

View File

@@ -2,6 +2,7 @@ package com.serliunx.ddns.support.ipprovider;
import com.serliunx.ddns.support.Assert; import com.serliunx.ddns.support.Assert;
import com.serliunx.ddns.support.InstanceContextHolder; import com.serliunx.ddns.support.InstanceContextHolder;
import com.serliunx.ddns.support.thread.ThreadFactoryBuilder;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -11,12 +12,13 @@ import java.util.function.Consumer;
/** /**
* 自动更新的ip供应器 * 自动更新的ip供应器
* <li> 异步更新ip, 获取到的ip地址不一定为最新可用的。 * <li> 异步更新ip, 获取到的ip地址不一定为最新可用的。
* <li> 也可作为简单的任务提交到线程池中执行.
* *
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a> * @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
* @version 1.0.3 * @version 1.0.3
* @since 2024/11/25 * @since 2024/11/25
*/ */
public class ScheduledProvider extends AbstractProvider { public class ScheduledProvider extends AbstractProvider implements AutoCloseable, Runnable {
private final Provider internalProvider; private final Provider internalProvider;
@@ -37,6 +39,10 @@ public class ScheduledProvider extends AbstractProvider {
* 处理器 * 处理器
*/ */
private Consumer<String> valueConsumer = null; private Consumer<String> valueConsumer = null;
/**
* 内置缓存
*/
private volatile String internalCache = null;
public ScheduledProvider(Provider internalProvider, long timePeriod) { public ScheduledProvider(Provider internalProvider, long timePeriod) {
Assert.notNull(internalProvider); Assert.notNull(internalProvider);
@@ -50,14 +56,26 @@ public class ScheduledProvider extends AbstractProvider {
this(internalProvider, 60); this(internalProvider, 60);
} }
@Override
public void close() {
poolExecutor.shutdown();
}
@Override
public void run() {
doAction();
}
@Override @Override
public String get() { public String get() {
return cache; return internalCache;
} }
@Override @Override
public void init() { public void init() {
poolExecutor = new ScheduledThreadPoolExecutor(2); poolExecutor = new ScheduledThreadPoolExecutor(2, ThreadFactoryBuilder.builder()
.ofNamePattern("ip-provider-%s")
);
// 提交 // 提交
submitTask(); submitTask();
} }
@@ -95,18 +113,32 @@ public class ScheduledProvider extends AbstractProvider {
* 提交任务逻辑 * 提交任务逻辑
*/ */
private void submitTask() { private void submitTask() {
task = poolExecutor.scheduleAtFixedRate(() -> { task = poolExecutor.scheduleAtFixedRate(this, 0, timePeriod, TimeUnit.SECONDS);
// 打断时, 终止已有的任务. (逻辑上不应该发生) }
if (Thread.currentThread().isInterrupted()) {
log.debug("上一个ip更新任务已终止.");
return;
}
InstanceContextHolder.setAdditional("ip-update");
cache = internalProvider.get();
if (valueConsumer != null) { /**
valueConsumer.accept(cache); * 执行逻辑
} */
}, 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,10 +21,9 @@ 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 static final int DEFAULT_OVERTIME = 3;
private HttpClient() {throw new UnsupportedOperationException();} private HttpClient() {throw new UnsupportedOperationException();}
@@ -39,29 +37,28 @@ public final class HttpClient {
} }
/** /**
* 获取本机的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;
} }

View File

@@ -20,11 +20,10 @@ import java.util.concurrent.locks.ReentrantLock;
public final class SQLiteConnector implements Refreshable { public final class SQLiteConnector implements Refreshable {
private volatile Connection connection; private volatile Connection connection;
private volatile boolean initialized = false;
private final Lock initLock = new ReentrantLock(); private final Lock initLock = new ReentrantLock();
private volatile boolean initialized = false;
private static final Logger log = LoggerFactory.getLogger(SQLiteConnector.class); private static final Logger log = LoggerFactory.getLogger(SQLiteConnector.class);
private static final SQLiteConnector INSTANCE = new SQLiteConnector(); private static final SQLiteConnector INSTANCE = new SQLiteConnector();
@@ -53,6 +52,9 @@ public final class SQLiteConnector implements Refreshable {
log.info("initialing sqlite connection."); log.info("initialing sqlite connection.");
connection = DriverManager.getConnection(SystemConstants.SQLITE_URL); connection = DriverManager.getConnection(SystemConstants.SQLITE_URL);
// 尝试创建数据库表, 只会执行一次
tryCreateTables();
initialized = true; initialized = true;
log.info("sqlite connection successfully initialized."); log.info("sqlite connection successfully initialized.");
} catch (Exception e) { } catch (Exception e) {
@@ -63,6 +65,16 @@ public final class SQLiteConnector implements Refreshable {
} }
} }
/**
* 尝试创建数据库表
* <li> 不存在时创建
*/
private void tryCreateTables() {
if (connection == null) {
throw new IllegalStateException("sql connection not initialized");
}
}
/** /**
* 是否已经初始化 * 是否已经初始化
*/ */

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,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;
@@ -33,13 +32,13 @@ public class ContextTest {
} }
@Test @Test
public void testFileContext(){ public void testFileContext() {
FileInstanceContext context = new FileInstanceContext(); FileInstanceContext context = new FileInstanceContext();
context.getSortedListableInstanceFactories().forEach(System.out::println); context.getSortedListableInstanceFactories().forEach(System.out::println);
} }
@Test @Test
public void testEmptyContext(){ public void testEmptyContext() {
GenericInstanceContext instanceContext = new GenericInstanceContext(false); GenericInstanceContext instanceContext = new GenericInstanceContext(false);
instanceContext.addListableInstanceFactory(new YamlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR)); instanceContext.addListableInstanceFactory(new YamlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR));

View File

@@ -2,12 +2,9 @@ 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.DatabaseInstanceFactory;
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;
import com.serliunx.ddns.support.sqlite.SQLiteConnector;
import org.junit.Test; import org.junit.Test;
import java.util.Map; import java.util.Map;
@@ -30,10 +27,4 @@ public class FactoryTest {
System.out.println(k + ": " + v); System.out.println(k + ": " + v);
}); });
} }
@Test
public void testDatabaseFactory() {
ListableInstanceFactory factory = new DatabaseInstanceFactory(SQLiteConnector.getInstance());
factory.refresh();
}
} }

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

@@ -1,14 +1,8 @@
package com.serliunx.ddns.test.support; package com.serliunx.ddns.test.support;
import com.serliunx.ddns.support.ipprovider.IpApiProvider;
import com.serliunx.ddns.support.ipprovider.Provider;
import com.serliunx.ddns.support.ipprovider.ScheduledProvider;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
/** /**
* 供应器测试 * 供应器测试
* //TODO 暂时移除,待重写单元测试
* *
* @author <a href="mailto:serliunx@yeah.net">SerLiunx</a> * @author <a href="mailto:serliunx@yeah.net">SerLiunx</a>
* @version 1.0.3 * @version 1.0.3
@@ -16,16 +10,4 @@ import java.util.concurrent.TimeUnit;
*/ */
public class ProviderTest { public class ProviderTest {
@Test
public void testIpApiProvider() {
Provider provider = new IpApiProvider();
System.out.println(provider.get());
}
@Test
public void testScheduledProvider() throws Exception {
ScheduledProvider provider = new ScheduledProvider(new IpApiProvider(), 3);
provider.changeTimePeriod(10);
TimeUnit.SECONDS.sleep(60);
}
} }