repos init.

This commit is contained in:
2024-05-15 17:52:24 +08:00
commit 478bebe66b
50 changed files with 3475 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
package com.serliunx.ddns;
import com.serliunx.ddns.config.PropertiesConfiguration;
import com.serliunx.ddns.constant.SystemConstants;
import com.serliunx.ddns.core.context.FileInstanceContext;
import com.serliunx.ddns.support.SystemInitializer;
import com.serliunx.ddns.support.SystemSupport;
import org.slf4j.MDC;
/**
* 启动类
* @author SerLiunx
* @since 1.0
*/
public final class BootStrap {
public static void main(String[] args){
beforeInit();
init();
}
private static void beforeInit(){
MDC.put("pid", SystemSupport.getPid());
}
private static void init(){
SystemInitializer systemInitializer = SystemInitializer
.configurer()
.configuration(new PropertiesConfiguration(SystemConstants.USER_SETTINGS_PROPERTIES_PATH))
.instanceContext(new FileInstanceContext())
.done();
systemInitializer.refresh();
}
}

View File

@@ -0,0 +1,127 @@
package com.serliunx.ddns.config;
import com.serliunx.ddns.support.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 配置信息的抽象实现, 定义公共逻辑
* @author SerLiunx
* @since 1.0
*/
public abstract class AbstractConfiguration implements Configuration {
private static final Logger log = LoggerFactory.getLogger(AbstractConfiguration.class);
protected final Map<String, String> valueMap = new LinkedHashMap<>(16);
private final Lock loadLock = new ReentrantLock();
public AbstractConfiguration() {}
@Override
public Integer getInteger(String key) {
Assert.notNull(key);
String v = valueMap.get(key);
return v == null ? null : Integer.valueOf(v);
}
@Override
public Integer getInteger(String key, Integer defaultValue) {
Assert.notNull(key, defaultValue);
Integer v = getInteger(key);
return v == null ? defaultValue : v;
}
@Override
public Long getLong(String key) {
Assert.notNull(key);
String v = valueMap.get(key);
return v == null ? null : Long.valueOf(v);
}
@Override
public Long getLong(String key, Long defaultValue) {
Assert.notNull(key, defaultValue);
Long v = getLong(key);
return v == null ? defaultValue : v;
}
@Override
public String getString(String key) {
Assert.notNull(key);
return valueMap.get(key);
}
@Override
public String getString(String key, String defaultValue) {
Assert.notNull(key, defaultValue);
String v = getString(key);
return v == null ? defaultValue : v;
}
@Override
public Boolean getBoolean(String key) {
Assert.notNull(key);
return Boolean.valueOf(valueMap.get(key));
}
@Override
public Boolean getBoolean(String key, Boolean defaultValue) {
Assert.notNull(key, defaultValue);
String value = valueMap.get(key);
return value == null ? defaultValue : Boolean.valueOf(value);
}
@Override
public void refresh() {
// 刷新配置信息
refresh0();
final Boolean needPrint = getBoolean(ConfigurationKeys.KEY_CFG_LOG_ONSTART);
if(needPrint)
printDetails();
}
@Override
public <T extends Enum<?>> Enum<?> getEnum(Class<T> clazz, String key) {
return null;
}
/**
* 载入配置信息请加锁
*/
protected void load(){
try {
loadLock.lock();
// 清空原有的配置信息
valueMap.clear();
load0();
}finally {
loadLock.unlock();
}
}
/**
* 打印配置信息
*/
protected void printDetails(){
log.info("=====配置信息=====");
valueMap.forEach((k, v) -> {
log.info("{} = {}", k, v);
});
log.info("=================");
}
/**
* 具体的刷新逻辑
*/
protected abstract void refresh0();
/**
* 载入逻辑
*/
protected abstract void load0();
}

View File

@@ -0,0 +1,72 @@
package com.serliunx.ddns.config;
import com.serliunx.ddns.support.Refreshable;
/**
* @author SerLiunx
* @since 1.0
*/
public interface Configuration extends Refreshable {
/**
* 获取整数
* @param key 键
* @return 整数
*/
Integer getInteger(String key);
/**
* 获取整数, 带默认值
* @param key 键
* @param defaultValue 默认值
* @return 整数
*/
Integer getInteger(String key, Integer defaultValue);
/**
* 获取长整数
* @param key 键
* @return 长整数
*/
Long getLong(String key);
/**
* 获取长整数
* @param key 键
* @param defaultValue 默认值
* @return 长整数
*/
Long getLong(String key, Long defaultValue);
/**
* 获取字符串
* @param key 键
* @return 字符串
*/
String getString(String key);
/**
* 获取字符串
* @param key 键
* @param defaultValue 默认值
* @return 字符串
*/
String getString(String key, String defaultValue);
/**
* 获取布尔值
* @param key 键
* @return 布尔值
*/
Boolean getBoolean(String key);
/**
* 获取布尔值
* @param key 键
* @param defaultValue 默认值
* @return 布尔值
*/
Boolean getBoolean(String key, Boolean defaultValue);
<T extends Enum<?>> Enum<?> getEnum(Class<T> clazz, String key);
}

View File

@@ -0,0 +1,26 @@
package com.serliunx.ddns.config;
/**
* 配置文件键常量信息
* @author SerLiunx
* @since 1.0
*/
public final class ConfigurationKeys {
private ConfigurationKeys(){throw new UnsupportedOperationException();}
/**
* 线程池核心线程数量
*/
public static final String KEY_THREAD_POOL_CORE_SIZE = "system.pool.core.size";
/**
* 启动时是否输出配置信息
*/
public static final String KEY_CFG_LOG_ONSTART = "system.cfg.log.onstart";
/**
* 定时任务周期: 获取最新IP
*/
public static final String KEY_TASK_REFRESH_INTERVAL_IP = "system.task.refresh.interval.ip";
}

View File

@@ -0,0 +1,59 @@
package com.serliunx.ddns.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* 使用{@link Properties}实现的简单读取键值对形式的配置信息实现
* @author SerLiunx
* @since 1.0
*/
public class PropertiesConfiguration extends AbstractConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesConfiguration.class);
private final String path;
private Properties properties;
public PropertiesConfiguration(String path) {
this.path = path;
}
@Override
protected void refresh0() {
this.properties = new Properties();
InputStream inputStream = null;
try {
inputStream = Files.newInputStream(Paths.get(path));
properties.load(inputStream);
// 载入配置信息
load();
} catch (IOException e) {
LOGGER.error("配置文件读取出现异常 => {}", e.toString());
}finally {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
LOGGER.error("配置文件资源释放出现异常 => {}", e.getMessage());
}
}
}
}
@Override
protected void load0() {
Set<Map.Entry<Object, Object>> entries = properties.entrySet();
entries.forEach(e -> {
valueMap.put((String) e.getKey(), (String) e.getValue());
});
}
}

View File

@@ -0,0 +1,33 @@
package com.serliunx.ddns.constant;
import com.serliunx.ddns.core.instance.AliyunInstance;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.core.instance.TencentInstance;
import java.util.HashMap;
import java.util.Map;
/**
* 实例类型集合
* @author SerLiunx
* @since 1.0
*/
public final class InstanceClasses {
private InstanceClasses(){throw new UnsupportedOperationException();}
private static final Map<InstanceType, Class<? extends Instance>> instanceTypeMap =
new HashMap<InstanceType, Class<? extends Instance>>(){
{
put(InstanceType.ALI_YUN, AliyunInstance.class);
put(InstanceType.TENCENT_CLOUD, TencentInstance.class);
}
};
public static Class<? extends Instance> match(InstanceType type){
return instanceTypeMap.get(type);
}
public static Class<? extends Instance> match(String type){
return instanceTypeMap.get(InstanceType.valueOf(type));
}
}

View File

@@ -0,0 +1,24 @@
package com.serliunx.ddns.constant;
/**
* 保存实例的文件类型: XML、JSON等
* @author SerLiunx
* @since 1.0
*/
public enum InstanceFileType {
XML(".xml"),
JSON(".json"),
YML(".yml"),
YAML(".yaml"),
;
private final String value;
public String getValue() {
return value;
}
InstanceFileType(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,33 @@
package com.serliunx.ddns.constant;
import static com.serliunx.ddns.constant.SystemConstants.*;
/**
* 实例来源
* @author SerLiunx
* @since 1.0
*/
public enum InstanceSource {
FILE_JSON(JSON_FILE),
FILE_XML(XML_FILE),
FILE_YML(YML),
DATABASE(DATABASE_SQLITE),
UNKNOWN("未知"),
;
/**
* 来源标签
* <li> 如果是从文件加载的实例信息, 标签即表示文件后缀名
* <li> 如果是来自数据库, 标签即表示数据库类型
*/
private final String sourceTag;
InstanceSource(String sourceTag) {
this.sourceTag = sourceTag;
}
public String getSourceTag() {
return sourceTag;
}
}

View File

@@ -0,0 +1,26 @@
package com.serliunx.ddns.constant;
/**
* 实例类型: 阿里云、华为云、腾讯云等
* @author SerLiunx
* @since 1.0
*/
public enum InstanceType {
/**
* 可继承的实例
* <li> 比较该类型为可继承的实例
* <li> 用于实例的某些参数可复用的情况
*/
INHERITED,
/**
* 阿里云
*/
ALI_YUN,
/**
* 腾讯云
*/
TENCENT_CLOUD,
}

View File

@@ -0,0 +1,68 @@
package com.serliunx.ddns.constant;
import java.io.File;
/**
* 系统常量
* @author SerLiunx
* @since 1.0
*/
public final class SystemConstants {
private SystemConstants(){throw new UnsupportedOperationException();}
/**
* 保存实例的文件夹
*/
public static final String INSTANCE_FOLDER_NAME = "instances";
/**
* 运行目录
*/
public static final String USER_DIR = System.getProperty("user.dir");
/**
* JSON文件后缀
*/
public static final String JSON_FILE = ".json";
/**
* XML文件后缀
*/
public static final String XML_FILE = ".xml";
/**
* YML文件后缀
*/
public static final String YML = ".yml";
/**
* properties配置文件名称
*/
public static final String PROPERTIES_FILE = "settings.properties";
/**
* sqlite
*/
public static final String DATABASE_SQLITE = "sqlite";
/**
* XML格式的实例文件根元素名称
*/
public static final String XML_ROOT_INSTANCE_NAME = "instance";
/**
* 实例类型字段名
*/
public final static String TYPE_FIELD = "type";
/**
* 用户目录下的实例存放位置
*/
public static final String USER_INSTANCE_DIR = USER_DIR + File.separator + INSTANCE_FOLDER_NAME;
/**
* 用户目录下的.properties配置文件
*/
public static final String USER_SETTINGS_PROPERTIES_PATH = USER_DIR + File.separator + PROPERTIES_FILE;
}

View File

@@ -0,0 +1,31 @@
package com.serliunx.ddns.core;
import java.io.File;
import java.io.FileFilter;
/**
* 文件过滤器, 用于加载过滤存储在文件中的实例信息时
* @author SerLiunx
* @since 1.0
* @see com.serliunx.ddns.core.factory.FileInstanceFactory
*/
public final class InstanceFileFilter implements FileFilter {
private final String[] fileSuffix;
public InstanceFileFilter(String[] fileSuffix) {
this.fileSuffix = fileSuffix;
}
@Override
public boolean accept(File pathname) {
if(!pathname.isFile())
return false;
for (String suffix : fileSuffix) {
if(pathname.getName().endsWith(suffix)){
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,18 @@
package com.serliunx.ddns.core;
/**
* 定义一个对象的优先级
* <li> 数字越大, 优先级越小
* @author SerLiunx
* @since 1.0
*/
@FunctionalInterface
public interface Priority {
/**
* 获取该对象的优先级
* <li> 数字越大, 优先级越小
* @return 优先级
*/
int getPriority();
}

View File

@@ -0,0 +1,153 @@
package com.serliunx.ddns.core.context;
import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.core.factory.ListableInstanceFactory;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.support.Assert;
import com.serliunx.ddns.support.Refreshable;
import com.serliunx.ddns.util.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
import static com.serliunx.ddns.util.InstanceUtils.validateInstance;
/**
* 实例容器的抽象实现, 定义大部分公共逻辑
* @author SerLiunx
* @since 1.0
*/
public abstract class AbstractInstanceContext implements InstanceContext, MultipleSourceInstanceContext {
private static final Logger log = LoggerFactory.getLogger(AbstractInstanceContext.class);
private final Set<ListableInstanceFactory> listableInstanceFactories = new HashSet<>();
/**
* 完整的实例信息
* <li> 作为主要操作对象
*/
private Map<String, Instance> instanceMap;
/**
* 实例信息缓存, 此时的实例继承关系并不完整
* <li> 不能作为主要的操作对象
* <li> 容器一般会在刷新完毕后清空该Map, 具体取决于容器本身
*/
private Map<String, Instance> cacheInstanceMap;
@Override
public void refresh() {
if(listableInstanceFactories.isEmpty())
return;
// 初始化所有实例工厂
listableInstanceFactories.stream()
.filter(f -> f != this)
.forEach(ListableInstanceFactory::refresh);
// 加载、过滤所有实例
Set<Instance> instances = new HashSet<>();
listableInstanceFactories.forEach(f -> instances.addAll(f.getInstances()));
// TODO 加载实例, 按照实例工厂的优先级从低到高优先级排, 高优先级的实例会覆盖低优先级的实例信息(如果存在重复的实例信息)
// 初次载入
cacheInstanceMap = new HashMap<>(instances.stream().collect(Collectors.toMap(Instance::getName, i -> i)));
Set<Instance> builtInstances = buildInstances(instances);
instanceMap = builtInstances.stream().collect(Collectors.toMap(Instance::getName, i -> i));
// 调用善后处理钩子函数
afterRefresh();
}
@Override
public boolean addInstance(Instance instance, boolean override) {
validateInstance(instance);
Instance i = instanceMap.get(instance.getName());
if(override && i != null){
return false;
}
instanceMap.put(instance.getName(), instance);
return true;
}
@Override
public void addInstance(Instance instance) {
addInstance(instance, false);
}
@Override
public Instance getInstance(String instanceName) {
Assert.notNull(instanceName);
final Instance instance = instanceMap.get(instanceName);
Assert.notNull(instance);
return instance;
}
@Override
public Set<Instance> getInstances() {
return instanceMap == null ? Collections.emptySet() : new HashSet<>(instanceMap.values());
}
@Override
public Map<String, Instance> getInstanceOfType(InstanceType type) {
Assert.notNull(instanceMap);
return instanceMap.values()
.stream()
.filter(i -> i.getType().equals(type))
.collect(Collectors.toMap(Instance::getName, i -> i));
}
@Override
public void addListableInstanceFactory(ListableInstanceFactory listableInstanceFactory) {
listableInstanceFactories.add(listableInstanceFactory);
}
@Override
public Set<ListableInstanceFactory> getListableInstanceFactories() {
return listableInstanceFactories;
}
/**
* 善后工作
*/
public abstract void afterRefresh();
/**
* 缓存清理
*/
protected void clearCache(){
int size = cacheInstanceMap.size();
cacheInstanceMap.clear();
log.debug("缓存信息清理 => {} 条", size);
// 清理实例工厂的缓存信息
listableInstanceFactories.forEach(Refreshable::afterRefresh);
}
/**
* 构建完整的实例信息
* @param instances 实例信息
* @return 属性设置完整的实例
*/
private Set<Instance> buildInstances(Collection<Instance> instances){
//设置实例信息, 如果需要从父类继承
return instances.stream()
.filter(i -> !InstanceType.INHERITED.equals(i.getType()))
.peek(i -> {
String fatherName = i.getFatherName();
if(fatherName != null && !fatherName.isEmpty()){
Instance fatherInstance = cacheInstanceMap.get(fatherName);
if(fatherInstance != null){
try {
ReflectionUtils.copyField(fatherInstance, i, true);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
})
.collect(Collectors.toCollection(HashSet::new));
}
}

View File

@@ -0,0 +1,27 @@
package com.serliunx.ddns.core.context;
import com.serliunx.ddns.constant.SystemConstants;
import com.serliunx.ddns.core.factory.JsonFileInstanceFactory;
import com.serliunx.ddns.core.factory.XmlFileInstanceFactory;
import com.serliunx.ddns.core.factory.YamlFileInstanceFactory;
/**
* 文件形式的实例容器
* @author SerLiunx
* @since 1.0
*/
public class FileInstanceContext extends AbstractInstanceContext {
public FileInstanceContext() {
addListableInstanceFactory(new JsonFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR));
addListableInstanceFactory(new XmlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR));
addListableInstanceFactory(new YamlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR));
// 刷新容器
refresh();
}
@Override
public void afterRefresh() {
clearCache();
}
}

View File

@@ -0,0 +1,14 @@
package com.serliunx.ddns.core.context;
/**
* 简易的容器实现, 需要手动进行刷新、添加实例工厂.
* @author SerLiunx
* @since 1.0
*/
public class GenericInstanceContext extends AbstractInstanceContext {
@Override
public void afterRefresh() {
clearCache();
}
}

View File

@@ -0,0 +1,16 @@
package com.serliunx.ddns.core.context;
import com.serliunx.ddns.core.factory.InstanceFactory;
import com.serliunx.ddns.support.Refreshable;
/**
* @author SerLiunx
* @since 1.0
*/
public interface InstanceContext extends InstanceFactory, Refreshable {
@Override
default int getPriority() {
return 0;
}
}

View File

@@ -0,0 +1,28 @@
package com.serliunx.ddns.core.context;
import com.serliunx.ddns.core.factory.InstanceFactory;
import com.serliunx.ddns.core.factory.ListableInstanceFactory;
import java.util.Set;
/**
* 多数据源的实例容器, 将多种实例来源汇聚到一起
* @see InstanceFactory
* @see InstanceContext
* @author SerLiunx
* @since 1.0
*/
public interface MultipleSourceInstanceContext extends InstanceContext, ListableInstanceFactory {
/**
* 添加一个实例工厂
* @param listableInstanceFactory 实例工厂
*/
void addListableInstanceFactory(ListableInstanceFactory listableInstanceFactory);
/**
* 获取所有实例工厂
* @return 实例工厂列表
*/
Set<ListableInstanceFactory> getListableInstanceFactories();
}

View File

@@ -0,0 +1,90 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.support.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
import static com.serliunx.ddns.util.InstanceUtils.validateInstance;
/**
* @author SerLiunx
* @since 1.0
*/
public abstract class AbstractInstanceFactory implements InstanceFactory, ListableInstanceFactory {
private static final Logger log = LoggerFactory.getLogger(AbstractInstanceFactory.class);
/**
* 实例信息
*/
private Map<String, Instance> instanceMap;
@Override
public Instance getInstance(String instanceName) {
Assert.notNull(instanceName);
final Instance instance = instanceMap.get(instanceName);
Assert.notNull(instance);
return instance;
}
@Override
public Set<Instance> getInstances() {
return instanceMap == null ? Collections.emptySet() : new HashSet<>(instanceMap.values());
}
@Override
public Map<String, Instance> getInstanceOfType(InstanceType type) {
Assert.notNull(instanceMap);
return instanceMap.values()
.stream()
.filter(i -> i.getType().equals(type))
.collect(Collectors.toMap(Instance::getName, i -> i));
}
@Override
public boolean addInstance(Instance instance, boolean override) {
validateInstance(instance);
Instance i = instanceMap.get(instance.getName());
if(override && i != null){
return false;
}
instanceMap.put(instance.getName(), instance);
return true;
}
@Override
public void addInstance(Instance instance) {
addInstance(instance, false);
}
@Override
public void refresh() {
Set<Instance> instances = load();
if(instances != null && !instances.isEmpty())
instanceMap = new HashMap<>(instances.stream()
.collect(Collectors.toMap(Instance::getName, i -> i)));
}
@Override
public int getPriority() {
return Integer.MAX_VALUE;
}
@Override
public void afterRefresh() {
int size = instanceMap.size();
instanceMap.clear();
log.debug("缓存信息清理 => {} 条", size);
}
/**
* 交由子类去加载实例信息
* @return 实例信息
*/
protected abstract Set<Instance> load();
}

View File

@@ -0,0 +1,19 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.core.instance.Instance;
import java.util.Collections;
import java.util.Set;
/**
* 数据库实例工厂
* @author SerLiunx
* @since 1.0
*/
public abstract class DatabaseInstanceFactory extends AbstractInstanceFactory{
@Override
protected Set<Instance> load() {
return Collections.emptySet();
}
}

View File

@@ -0,0 +1,76 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.core.InstanceFileFilter;
import com.serliunx.ddns.core.instance.Instance;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author SerLiunx
* @since 1.0
*/
public abstract class FileInstanceFactory extends AbstractInstanceFactory {
/**
* 存储实例信息的文件夹路径
*/
protected String instanceDir;
public FileInstanceFactory(String instanceDir) {
this.instanceDir = instanceDir;
}
@Override
protected Set<Instance> load() {
Set<File> files = loadFiles();
if(files != null && !files.isEmpty()){
return files.stream()
.map(this::loadInstance)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(HashSet::new));
}
return Collections.emptySet();
}
@Override
public int getPriority() {
return 256;
}
/**
* 交由具体的子类去加载实例, 比如: json格式的实例信息、xml格式的实例信息
* @param file 文件信息
* @return 实例
*/
protected abstract Instance loadInstance(File file);
/**
* 子类要设置自己可以加载的文件后缀名
* <li> 后缀名仅仅是一个标记符, 文件不一定要有后缀名哦
* @return 文件后缀名
*/
protected abstract String[] fileSuffix();
/**
* 载入目录下所有符合条件的文件
*/
private Set<File> loadFiles(){
File pathFile = new File(instanceDir);
if(!pathFile.exists()){
boolean result = pathFile.mkdirs();
if(!result){
throw new IllegalArgumentException("create path failed");
}
}
if(!pathFile.isDirectory()){
throw new IllegalArgumentException("path is not a directory");
}
File[] files = pathFile.listFiles(new InstanceFileFilter(fileSuffix()));
if(files == null || files.length == 0){
return Collections.emptySet();
}
return Arrays.stream(files).collect(Collectors.toCollection(HashSet::new));
}
}

View File

@@ -0,0 +1,44 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.core.Priority;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.support.Refreshable;
/**
* @author SerLiunx
* @since 1.0
*/
public interface InstanceFactory extends Priority, Comparable<InstanceFactory>, Refreshable {
/**
* 添加实例
* <li> 此方法默认为不覆盖的方式添加, 即如果存在则添加失败, 没有任何返回值和异常.
* @param instance 实例信息
*/
void addInstance(Instance instance);
/**
* 添加实例
* @param instance 实例信息
* @param override 是否覆盖原有的同名实例
* @return 成功添加返回真, 否则返回假
*/
boolean addInstance(Instance instance, boolean override);
/**
* 根据实例名称获取实例
* @param instanceName 实例名称
* @return 实例信息, 如果不存在则会抛出异常
*/
Instance getInstance(String instanceName);
@Override
default int compareTo(InstanceFactory o) {
if(getPriority() < o.getPriority()){
return 1;
} else if (this.getPriority() > o.getPriority()) {
return -1;
}
return 0;
}
}

View File

@@ -0,0 +1,49 @@
package com.serliunx.ddns.core.factory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.constant.SystemConstants;
import com.serliunx.ddns.core.instance.Instance;
import java.io.File;
import static com.serliunx.ddns.constant.InstanceClasses.match;
/**
* Jackson文件实例工厂, 使用jackson的ObjectMapper来分别处理json和xml
* @author SerLiunx
* @since 1.0
* @see ObjectMapper
* @see com.fasterxml.jackson.dataformat.xml.XmlMapper
* @see com.fasterxml.jackson.databind.json.JsonMapper
*/
public abstract class JacksonFileInstanceFactory extends FileInstanceFactory{
private final ObjectMapper objectMapper;
public JacksonFileInstanceFactory(String instanceDir, ObjectMapper objectMapper) {
super(instanceDir);
this.objectMapper = objectMapper;
}
@Override
protected Instance loadInstance(File file) {
try{
JsonNode root = objectMapper.readTree(file);
String rootName = root.get(SystemConstants.TYPE_FIELD).asText(); //根据类型去装配实例信息
InstanceType instanceType = InstanceType.valueOf(rootName);
return post(objectMapper.treeToValue(root, match(instanceType)));
}catch (Exception e){
throw new RuntimeException(e);
}
}
@Override
protected abstract String[] fileSuffix();
/**
* 处理后续逻辑
*/
protected abstract Instance post(Instance instance);
}

View File

@@ -0,0 +1,37 @@
package com.serliunx.ddns.core.factory;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.serliunx.ddns.constant.InstanceSource;
import com.serliunx.ddns.core.instance.Instance;
/**
* Jackson-Json文件实例工厂
* @author SerLiunx
* @since 1.0
*/
public class JsonFileInstanceFactory extends JacksonFileInstanceFactory{
public JsonFileInstanceFactory(String instanceDir, JsonMapper jsonMapper) {
super(instanceDir, jsonMapper);
}
public JsonFileInstanceFactory(String instanceDir) {
this(instanceDir, new JsonMapper());
}
@Override
public int getPriority() {
return 1;
}
@Override
protected String[] fileSuffix() {
return new String[]{".json"};
}
@Override
protected Instance post(Instance instance) {
instance.setSource(InstanceSource.FILE_JSON);
return instance;
}
}

View File

@@ -0,0 +1,36 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.core.instance.Instance;
import java.util.Map;
import java.util.Set;
/**
* @author SerLiunx
* @since 1.0
*/
public interface ListableInstanceFactory extends InstanceFactory {
/**
* 获取所有已加载的实例信息
* @return 所有实例信息
*/
Set<Instance> getInstances();
/**
* 获取指定类型的实例
* @param type 类型
* @return 实例名称-实例信息 键值对.
*/
Map<String, Instance> getInstanceOfType(InstanceType type);
/**
* 获取指定类型的实例
* @param type 类型名称
* @return 实例名称-实例信息 键值对.
*/
default Map<String, Instance> getInstanceOfType(String type) {
return getInstanceOfType(InstanceType.valueOf(type));
}
}

View File

@@ -0,0 +1,37 @@
package com.serliunx.ddns.core.factory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.serliunx.ddns.constant.InstanceSource;
import com.serliunx.ddns.core.instance.Instance;
/**
* Jackson-Xml文件实例工厂
* @author SerLiunx
* @since 1.0
*/
public class XmlFileInstanceFactory extends JacksonFileInstanceFactory{
public XmlFileInstanceFactory(String instanceDir, XmlMapper xmlMapper) {
super(instanceDir, xmlMapper);
}
public XmlFileInstanceFactory(String instanceDir) {
this(instanceDir, new XmlMapper());
}
@Override
public int getPriority() {
return 2;
}
@Override
protected String[] fileSuffix() {
return new String[]{".xml"};
}
@Override
protected Instance post(Instance instance) {
instance.setSource(InstanceSource.FILE_XML);
return instance;
}
}

View File

@@ -0,0 +1,104 @@
package com.serliunx.ddns.core.factory;
import com.serliunx.ddns.constant.InstanceClasses;
import com.serliunx.ddns.constant.InstanceSource;
import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.util.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Map;
import static com.serliunx.ddns.constant.SystemConstants.TYPE_FIELD;
/**
* @author SerLiunx
* @since 1.0
*/
public class YamlFileInstanceFactory extends FileInstanceFactory {
private static final Logger logger = LoggerFactory.getLogger(YamlFileInstanceFactory.class);
public YamlFileInstanceFactory(String instanceDir) {
super(instanceDir);
}
@Override
public int getPriority() {
return 3;
}
@Override
protected Instance loadInstance(File file) {
FileInputStream instanceInputStream = null;
try {
instanceInputStream = new FileInputStream(file);
Yaml yaml = new Yaml();
Map<String, Object> valueMap = yaml.load(instanceInputStream);
InstanceType type = null;
if (valueMap.get(TYPE_FIELD) != null) {
type = InstanceType.valueOf((String) valueMap.get(TYPE_FIELD));
}
if (type == null) {
logger.error("文件 {} 读取失败, 可能是缺少关键参数.", file.getName());
return null;
}
Class<? extends Instance> clazz = InstanceClasses.match(type);
if (clazz != null) {
Constructor<? extends Instance> constructor = clazz.getConstructor();
Instance instance = buildInstance(constructor.newInstance(), valueMap);
instance.setSource(InstanceSource.FILE_YML);
return instance;
}
return null;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (instanceInputStream != null) {
instanceInputStream.close();
}
} catch (IOException e) {
logger.error("文件读取出现异常.");
}
}
}
@Override
protected String[] fileSuffix() {
return new String[]{".yml", ".yaml"};
}
@SuppressWarnings(value = {"unchecked", "rawtypes"})
protected Instance buildInstance(Instance instance, Map<String, Object> valueMap){
Field[] declaredFields = ReflectionUtils.getDeclaredFields(instance.getClass(), true);
for (Field f : declaredFields) {
if (Modifier.isStatic(f.getModifiers())) {
continue;
}
Object value = valueMap.get(f.getName());
f.setAccessible(true);
try {
//设置枚举类
Class<?> clazz = f.getType();
if (clazz.isEnum() && value != null) {
f.set(instance, Enum.valueOf((Class<? extends Enum>) clazz, (String) value));
continue;
}
f.set(instance, value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
f.setAccessible(false);
}
return instance;
}
}

View File

@@ -0,0 +1,137 @@
package com.serliunx.ddns.core.instance;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.serliunx.ddns.constant.InstanceSource;
import com.serliunx.ddns.constant.InstanceType;
import static com.serliunx.ddns.constant.SystemConstants.XML_ROOT_INSTANCE_NAME;
/**
* @author SerLiunx
* @since 1.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@JacksonXmlRootElement(localName = XML_ROOT_INSTANCE_NAME)
public abstract class AbstractInstance implements Instance {
/**
* 实例名称
* <li> 全局唯一
*/
protected String name;
/**
* 父实例名称
*/
protected String fatherName;
/**
* 执行周期
*/
protected Long interval;
/**
* 实例类型
*/
protected InstanceType type;
/**
* 实例来源
*/
protected InstanceSource source;
/**
* 获取到的ip地址. 仅做记录, 不需要手动设定
*/
protected String value;
@Override
public void refresh() {
// 调用子类的初始化逻辑
init();
}
@Override
public void run() {
if(query())
run0();
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
public String getFatherName() {
return fatherName;
}
public void setFatherName(String fatherName) {
this.fatherName = fatherName;
}
public Long getInterval() {
return interval;
}
public void setInterval(Long interval) {
this.interval = interval;
}
@Override
public InstanceType getType() {
return type;
}
@Override
public void setType(InstanceType instanceType) {
this.type = instanceType;
}
@Override
public void setSource(InstanceSource instanceSource) {
this.source = instanceSource;
}
public InstanceSource getSource() {
return source;
}
@Override
public boolean validate() {
// 校验通用参数, 具体子类的参数交由子类校验
if(name == null || name.isEmpty() || interval <= 0 || type == null){
return false;
}
return validate0();
}
/**
* 具体的初始化逻辑
*/
protected abstract void init();
/**
* 子类参数校验
*/
protected abstract boolean validate0();
/**
* 更新前检查是否需要更新
* @return 无需更新返回假, 否则返回真
*/
protected abstract boolean query();
/**
* 具体执行逻辑
*/
protected abstract void run0();
}

View File

@@ -0,0 +1,227 @@
package com.serliunx.ddns.core.instance;
import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.service.alidns20150109.AsyncClient;
import com.aliyun.sdk.service.alidns20150109.models.DescribeDomainRecordInfoRequest;
import com.aliyun.sdk.service.alidns20150109.models.DescribeDomainRecordInfoResponse;
import com.aliyun.sdk.service.alidns20150109.models.DescribeDomainRecordInfoResponseBody;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.serliunx.ddns.support.NetworkContextHolder;
import darabonba.core.client.ClientOverrideConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.serliunx.ddns.constant.SystemConstants.XML_ROOT_INSTANCE_NAME;
/**
* 阿里云实例定义
* @author SerLiunx
* @since 1.0
*/
@SuppressWarnings("all")
@JacksonXmlRootElement(localName = XML_ROOT_INSTANCE_NAME)
public class AliyunInstance extends AbstractInstance {
private static final Logger log = LoggerFactory.getLogger(AliyunInstance.class);
/**
* AccessKey ID
*/
private String accessKeyId;
/**
* AccessKey Secret
*/
private String accessKeySecret;
/**
* 解析记录ID
*/
private String recordId;
/**
* 主机记录。
* 如果要解析@.example.com主机记录要填写”@”,而不是空。
* 示例值:
* www
*/
private String rr;
/**
* 解析记录类型
* <li>A记录 A 参考标准RR值可为空即@解析;不允许含有下划线; IPv4地址格式
* <li>NS记录 NS 参考标准RR值不能为空允许含有下划线不支持泛解析 NameType形式
* <li>MX记录 MX 参考标准RR值可为空即@解析;不允许含有下划线 NameType形式且不可为IP地址。1-10优先级依次递减。
* <li>TXT记录 TXT 参考标准;另外,有效字符除字母、数字、“-”中横杠、还包括“_”下划线RR值可为空即@解析;允许含有下划线;
* 不支持泛解析 字符串长度小于512,合法字符:大小写字母,数字,空格,及以下字符:-~=:;/.@+^!*
* <li>CNAME记录 CNAME 参考标准;另外,有效字符除字母、数字、“-”中横杠、还包括“_”下划线RR值不允许为空即@
* 允许含有下划线 NameType形式且不可为IP
* <li>SRV记录 SRV 是一个name且可含有下划线“_“和点“.”;允许含有下划线;可为空(即@);不支持泛解析 priority优先级
* 为065535之间的数字weight权重为065535之间的数字port提供服务的端口号为065535之间的数字 target为提供服务的目标地址
* 为nameType且存在。参考<a href="https://en.wikipedia.org/wiki/SRV_record">...</a>
* <a href="http://www.rfc-editor.org/rfc/rfc2782.txt">...</a>
* <li>AAAA记录 AAAA 参考标准RR值可为空即@解析;不允许含有下划线; IPv6地址格式
* <li>CAA记录 CAA 参考标准RR值可为空即@解析;不允许含有下划线; 格式为:[flag] [tag] [value],是由一个标志字节的[flag],
* 和一个被称为属性的标签[tag]-值[value]对组成。例如:@ 0 issue "symantec.com"或@ 0 iodef "mailto:admin@aliyun.com"
* <li>显性URL转发 REDIRECT_URL 参考标准RR值可为空即@解析 NameType或URL地址区分大小写长度最长为500字符
* 其中域名如example.com必须大小写不敏感协议可选如HTTP、HTTPS默认为HTTP端口可选如81默认为80路径可选大小写敏感
* 如/path/to/,默认为/文件名可选大小写敏感如file.php默认无参数可选大小写敏感如?user=my***,默认无。
* <li>隐性URL转发 FORWARD_URL 参考标准RR值可为空即@解析 NameType或URL地址区分大小写长度最长为500字符其中域名
* 如example.com必须大小写不敏感协议可选如HTTP、HTTPS默认为HTTP端口可选如81默认为80路径可选大小写敏感
* 如/path/to/,默认为/文件名可选大小写敏感如file.php默认无参数可选大小写敏感如?user=my***,默认无。
*/
private String recordType;
@JsonIgnore
private AsyncClient client;
@JsonIgnore
private JsonMapper jsonMapper;
@Override
protected void init() {
jsonMapper = new JsonMapper();
StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
.accessKeyId(accessKeyId)
.accessKeySecret(accessKeySecret)
.build());
client = AsyncClient.builder()
.region("cn-hangzhou")
.credentialsProvider(provider)
.overrideConfiguration(
ClientOverrideConfiguration.create()
.setEndpointOverride("alidns.cn-hangzhou.aliyuncs.com")
)
.build();
debug("初始化完成.");
}
@Override
protected void run0() {
log("test");
}
@Override
protected boolean query() {
debug("正在校验是否需要更新记录.");
DescribeDomainRecordInfoRequest describeDomainRecordInfoRequest = DescribeDomainRecordInfoRequest.builder()
.recordId(recordId)
.build();
CompletableFuture<DescribeDomainRecordInfoResponse> responseCompletableFuture =
client.describeDomainRecordInfo(describeDomainRecordInfoRequest);
try {
DescribeDomainRecordInfoResponse response = responseCompletableFuture.get(5, TimeUnit.SECONDS);
DescribeDomainRecordInfoResponseBody body = response.getBody();
if(body != null){
String recordValue = body.getValue();
String ipAddress = NetworkContextHolder.getIpAddress();
debug("当前记录值 => {}", recordValue);
boolean result = !(recordValue != null && !recordValue.isEmpty()
&& recordValue.equals(ipAddress));
if(result)
debug("需要更新IP地址: {} => {}", recordValue, ipAddress);
else
debug("无需更新.");
return result;
}
return false;
} catch (InterruptedException | ExecutionException e) {
error("出现了不应该出现的异常 => {}", e);
return false;
} catch (TimeoutException e) {
error("记录查询超时! 将跳过查询直接执行更新操作.");
return true;
}
}
@Override
protected boolean validate0() {
//简单的必填参数校验
return accessKeyId != null && !accessKeyId.isEmpty() && accessKeySecret != null && !accessKeySecret.isEmpty()
&& recordId != null && !recordId.isEmpty() && rr != null && !rr.isEmpty() && recordType != null;
}
public String getAccessKeyId() {
return accessKeyId;
}
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public void setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
}
public String getRecordId() {
return recordId;
}
public void setRecordId(String recordId) {
this.recordId = recordId;
}
public String getRr() {
return rr;
}
public void setRr(String rr) {
this.rr = rr;
}
public String getRecordType() {
return recordType;
}
public void setRecordType(String recordType) {
this.recordType = recordType;
}
public AsyncClient getClient() {
return client;
}
public void setClient(AsyncClient client) {
this.client = client;
}
public JsonMapper getJsonMapper() {
return jsonMapper;
}
public void setJsonMapper(JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}
private void handleThrowable(Throwable t){
error("出现异常 {}:", t.getCause(), t.getMessage());
}
@SuppressWarnings("all")
private void log(String msg, Object...params){
log.info("[实例活动][" + name + "]" + msg, params);
}
@SuppressWarnings("all")
private void debug(String msg, Object...params){
log.debug("[实例活动][" + name + "]" + msg, params);
}
@SuppressWarnings("all")
private void error(String msg, Object...params){
log.error("[实例异常][" + name + "]" + msg, params);
}
}

View File

@@ -0,0 +1,78 @@
package com.serliunx.ddns.core.instance;
import com.serliunx.ddns.constant.InstanceSource;
import com.serliunx.ddns.constant.InstanceType;
import com.serliunx.ddns.support.Refreshable;
/**
* @author SerLiunx
* @since 1.0
*/
public interface Instance extends Runnable, Refreshable {
/**
* 获取实例名称
* @return 实例名称
*/
String getName();
/**
* 设置实例名称
* @param name 实例名称
*/
void setName(String name);
/**
* 获取父实例名称
* @return 父实例名称
*/
String getFatherName();
/**
* 设置父实例名称
* @param fatherName 父实例名称
*/
void setFatherName(String fatherName);
/**
* 获取实例执行周期 (单位秒)
* @return 执行周期
*/
Long getInterval();
/**
* 设置实例执行周期 (单位秒)
* @param interval 执行周期
*/
void setInterval(Long interval);
/**
* 获取实例类型
* @return 实例类型
*/
InstanceType getType();
/**
* 设置实例类型
* @param instanceType 实例类型
*/
void setType(InstanceType instanceType);
/**
* 获取实例来源
* @return 实例来源
*/
InstanceSource getSource();
/**
* 设置实例来源
* @param instanceSource 实例来源
*/
void setSource(InstanceSource instanceSource);
/**
* 实例参数校验
* @return 通过校验返回真, 否则返回假
*/
boolean validate();
}

View File

@@ -0,0 +1,38 @@
package com.serliunx.ddns.core.instance;
/**
* @author SerLiunx
* @since 1.0
*/
public class TencentInstance extends AbstractInstance {
@Override
protected void init() {
}
@Override
protected void run0() {
}
@Override
protected boolean query() {
return false;
}
@Override
protected boolean validate0() {
return false;
}
@Override
public String toString() {
return "TencentInstance{" +
"source=" + source +
", type=" + type +
", fatherName='" + fatherName + '\'' +
", name='" + name + '\'' +
'}';
}
}

View File

@@ -0,0 +1,52 @@
package com.serliunx.ddns.support;
import java.util.Collection;
/**
* 断言
* @author SerLiunx
* @since 1.0
*/
public final class Assert {
private Assert(){throw new UnsupportedOperationException();}
public static void notNull(Object object){
notNull(object, null);
}
public static void notNull(Object object, String msg){
if(object == null)
throw new NullPointerException(msg);
}
public static void notNull(Object...objects){
for (Object object : objects) {
notNull(object);
}
}
public static void isPositive(int i){
if(i <= 0){
throw new IllegalArgumentException("指定参数必须大于0!");
}
}
public static void isLargerThan(int source, int target){
if(source <= target){
throw new IllegalArgumentException(String.format("%s太小了, 它必须大于%s", source, target));
}
}
public static void notEmpty(Collection<?> collection){
notNull(collection);
if(collection.isEmpty())
throw new IllegalArgumentException("参数不能为空!");
}
public static void notEmpty(CharSequence charSequence){
notNull(charSequence);
if(charSequence.length() == 0)
throw new IllegalArgumentException("参数不能为空!");
}
}

View File

@@ -0,0 +1,33 @@
package com.serliunx.ddns.support;
import com.serliunx.ddns.config.Configuration;
import com.serliunx.ddns.core.context.MultipleSourceInstanceContext;
/**
* @author SerLiunx
* @since 1.0
*/
public final class Configurer {
private Configuration configuration;
private MultipleSourceInstanceContext instanceContext;
Configurer(){}
public Configurer configuration(Configuration configuration){
Assert.notNull(configuration);
this.configuration = configuration;
return this;
}
public Configurer instanceContext(MultipleSourceInstanceContext instanceContext){
Assert.notNull(instanceContext);
this.instanceContext = instanceContext;
return this;
}
public SystemInitializer done(){
Assert.notNull(configuration, instanceContext);
return new SystemInitializer(configuration, instanceContext);
}
}

View File

@@ -0,0 +1,56 @@
package com.serliunx.ddns.support;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 网络参数上下文, 目前仅用于存储本机网络IP
* @author SerLiunx
* @since 1.0
*/
public final class NetworkContextHolder {
private static final Logger log = LoggerFactory.getLogger(NetworkContextHolder.class);
private static final Lock IP_LOCK = new ReentrantLock();
// 防止初始化未完成.
private static final CountDownLatch IP_CONTEXT_WAIT_LATCH = new CountDownLatch(1);
// 外网IP地址获取
private static final Integer IP_CONTEXT_TIME_OUT = 5;
private static volatile String IP_ADDRESS;
private NetworkContextHolder(){throw new UnsupportedOperationException();}
public static void setIpAddress(String i){
try {
IP_LOCK.lock();
IP_ADDRESS = i;
if(IP_CONTEXT_WAIT_LATCH.getCount() > 0){
IP_CONTEXT_WAIT_LATCH.countDown();
}
}finally {
IP_LOCK.unlock();
}
}
public static String getIpAddress(){
log.debug("正在尝试获取最新的IP地址.");
if(IP_ADDRESS != null)
return IP_ADDRESS;
try {
if(!IP_CONTEXT_WAIT_LATCH.await(IP_CONTEXT_TIME_OUT, TimeUnit.SECONDS)){
log.error("IP地址获取超时.");
return null;
}
log.debug("最新的IP地址获取成功.");
return IP_ADDRESS;
} catch (InterruptedException e) {
log.error("IP地址获取出现异常 => {}", e.getMessage());
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
package com.serliunx.ddns.support;
/**
* 刷新逻辑
* @author SerLiunx
* @since 1.0
*/
@FunctionalInterface
public interface Refreshable {
/**
* 刷新(初始化)
*/
void refresh();
/**
* 刷新后逻辑定义, 一般用于资源清理
*/
default void afterRefresh(){
}
}

View File

@@ -0,0 +1,162 @@
package com.serliunx.ddns.support;
import com.serliunx.ddns.config.Configuration;
import com.serliunx.ddns.constant.SystemConstants;
import com.serliunx.ddns.core.context.MultipleSourceInstanceContext;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.support.feign.client.IPAddressClient;
import com.serliunx.ddns.support.feign.client.entity.IPAddressResponse;
import com.serliunx.ddns.thread.TaskThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.serliunx.ddns.config.ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP;
import static com.serliunx.ddns.config.ConfigurationKeys.KEY_THREAD_POOL_CORE_SIZE;
/**
* 系统初始化
* @author SerLiunx
* @since 1.0
*/
public final class SystemInitializer implements Refreshable{
private static final Logger log = LoggerFactory.getLogger(SystemInitializer.class);
private final Configuration configuration;
private final MultipleSourceInstanceContext instanceContext;
private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
private Set<Instance> instances;
SystemInitializer(Configuration configuration, MultipleSourceInstanceContext instanceContext) {
this.configuration = configuration;
this.instanceContext = instanceContext;
}
public static Configurer configurer(){
return new Configurer();
}
@Override
public void refresh() {
log.info("程序正在初始化, 请稍候.");
// 释放配置文件
releaseResource(SystemConstants.PROPERTIES_FILE);
// 刷新配置信息
configuration.refresh();
// 获取核心线程数量, 默认为CPU核心数量
int coreSize = configuration.getInteger(KEY_THREAD_POOL_CORE_SIZE, Runtime.getRuntime().availableProcessors());
// 初始化线程池
initThreadPool(coreSize);
// 加载实例(不同的容器加载时机不同)
loadInstances();
// 运行实例
runInstances();
log.info("初始化完成!");
}
public MultipleSourceInstanceContext getInstanceContext() {
return instanceContext;
}
public Set<Instance> getInstances() {
return instances;
}
@Override
public void afterRefresh() {
// TODO
}
private void loadInstances() {
instances = instanceContext.getInstances();
log.info("载入 {} 个实例.", instances.size());
}
private void releaseResource(String resourceName){
ClassLoader classLoader = SystemConstants.class.getClassLoader();
Path path = Paths.get(SystemConstants.USER_DIR + File.separator + resourceName);
// 检查文件是否已存在
if(Files.exists(path)){
log.debug("文件 {} 已存在, 无需解压.", resourceName);
return;
}
try (InputStream inputStream = classLoader.getResourceAsStream(resourceName)) {
log.debug("正在解压文件 {} 至路径: {}", resourceName, SystemConstants.USER_DIR);
// 创建输出流,写入文件到指定目录
OutputStream outputStream = Files.newOutputStream(path);
byte[] buffer = new byte[1024];
int bytesRead;
if(inputStream != null){
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
outputStream.close();
}catch (Exception e){
log.error("文件 {} 解压失败!, 原因: {}", resourceName, e.getMessage());
}
}
private void runInstances() {
Assert.notNull(scheduledThreadPoolExecutor);
Assert.notNull(instances);
for (Instance i : instances) {
if(!i.validate()){
log.error("实例{}({})参数校验不通过, 将不会被运行.", i.getName(), i.getType());
continue;
}
// 初始化实例
i.refresh();
scheduledThreadPoolExecutor.scheduleWithFixedDelay(i, 0, i.getInterval(), TimeUnit.SECONDS);
log.info("{}({})已启动, 运行周期 {} 秒.", i.getName(), i.getType(), i.getInterval());
}
}
private void initThreadPool(int coreSize){
Assert.isLargerThan(coreSize, 1);
scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(coreSize, new TaskThreadFactory());
// 初始化一个线程保活
scheduledThreadPoolExecutor.submit(() -> {});
// 提交定时获取网络IP的定时任务
scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
log.info("正在尝试获取本机最新的IP地址.");
IPAddressResponse response = IPAddressClient.instance.getIPAddress();
String ip;
if(response != null
&& (ip = response.getQuery()) != null){
NetworkContextHolder.setIpAddress(ip);
log.info("本机最新公网IP地址 => {}", ip);
}
}, 0, configuration.getLong(KEY_TASK_REFRESH_INTERVAL_IP, 300L), TimeUnit.SECONDS);
// 添加进程结束钩子函数
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
MDC.put("pid", SystemSupport.getPid());
log.info("程序正在关闭中, 可能需要一定时间.");
afterRefresh();
scheduledThreadPoolExecutor.shutdown();
log.info("已关闭.");
}, "DDNS-ShutDownHook"));
}
}

View File

@@ -0,0 +1,22 @@
package com.serliunx.ddns.support;
import java.lang.management.ManagementFactory;
/**
* @author SerLiunx
* @since 1.0
*/
public final class SystemSupport {
private static final String PID;
static{
PID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
}
private SystemSupport(){throw new UnsupportedOperationException();}
public static String getPid(){
return PID;
}
}

View File

@@ -0,0 +1,72 @@
package com.serliunx.ddns.support.feign;
import com.fasterxml.jackson.databind.*;
import feign.FeignException;
import feign.Response;
import feign.Util;
import feign.codec.Decoder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.Collections;
/**
* feign解码器
* @author SerLiunx
* @since 1.0
*/
public class JacksonDecoder implements Decoder {
private final ObjectMapper mapper;
private static final JacksonDecoder decoder = new JacksonDecoder();
private JacksonDecoder() {
this(Collections.emptyList());
}
private JacksonDecoder(Iterable<Module> modules) {
this(new ObjectMapper()
//设置下划线自动转化为驼峰命名
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModules(modules));
}
private JacksonDecoder(ObjectMapper mapper) {
this.mapper = mapper;
}
public static Decoder getInstance(){
return decoder;
}
@Override
public Object decode(Response response, Type type) throws FeignException, IOException {
if (response.status() == 404 || response.status() == 204)
return Util.emptyValueOf(type);
if (response.body() == null)
return null;
Reader reader = response.body().asReader(response.charset());
if (!reader.markSupported()) {
reader = new BufferedReader(reader, 1);
}
//处理响应体字符流
try{
reader.mark(1);
if (reader.read() == -1) {
return null;
}
reader.reset();
return mapper.readValue(reader, mapper.constructType(type));
} catch (RuntimeJsonMappingException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
throw e;
}finally {
response.close();
}
}
}

View File

@@ -0,0 +1,55 @@
package com.serliunx.ddns.support.feign;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import feign.RequestTemplate;
import feign.Util;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import java.lang.reflect.Type;
import java.util.Collections;
/**
* Feign兼容Jackson(反序列化返回值)
* @author SerLiunx
* @since 1.0
*/
public class JacksonEncoder implements Encoder {
private final ObjectMapper mapper;
private static final JacksonEncoder encoder = new JacksonEncoder();
private JacksonEncoder() {
this(Collections.emptyList());
}
private JacksonEncoder(Iterable<Module> modules) {
this(new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true)
.registerModules(modules));
}
private JacksonEncoder(ObjectMapper mapper) {
this.mapper = mapper;
}
public static Encoder getInstance(){
return encoder;
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
try {
JavaType javaType = mapper.getTypeFactory().constructType(bodyType);
template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8);
} catch (JsonProcessingException e) {
throw new EncodeException(e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,40 @@
package com.serliunx.ddns.support.feign.client;
import com.serliunx.ddns.support.feign.JacksonDecoder;
import com.serliunx.ddns.support.feign.JacksonEncoder;
import com.serliunx.ddns.support.feign.client.entity.IPAddressResponse;
import feign.Feign;
import feign.Request;
import feign.RequestLine;
import java.util.concurrent.TimeUnit;
/**
* 本机外网IP地址获取
* @author SerLiunx
* @since 1.0
*/
@SuppressWarnings("all")
public interface IPAddressClient {
static final String url = "http://ip-api.com";
static final IPAddressClient instance = getInstance();
/**
* 获取本机外网IP地址
* @return IPAddressResponse
*/
@RequestLine("GET /json")
IPAddressResponse getIPAddress();
static IPAddressClient getInstance(){
return Feign.builder()
.encoder(JacksonEncoder.getInstance())
.decoder(JacksonDecoder.getInstance())
.options(new Request.Options(10,
TimeUnit.SECONDS, 10,
TimeUnit.SECONDS, true))
.target(IPAddressClient.class, url);
}
}

View File

@@ -0,0 +1,156 @@
package com.serliunx.ddns.support.feign.client.entity;
/**
* IP地址查询响应
* @author SerLiunx
* @since 1.0
*/
@SuppressWarnings("all")
public class IPAddressResponse {
private String query;
private String status;
private String country;
private String countryCode;
private String region;
private String regionName;
private String city;
private String zip;
private String lat;
private String lon;
private String timezone;
private String isp;
private String org;
private String as;
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getCountryCode() {
return countryCode;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public String getRegionName() {
return regionName;
}
public void setRegionName(String regionName) {
this.regionName = regionName;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public String getLat() {
return lat;
}
public void setLat(String lat) {
this.lat = lat;
}
public String getLon() {
return lon;
}
public void setLon(String lon) {
this.lon = lon;
}
public String getTimezone() {
return timezone;
}
public void setTimezone(String timezone) {
this.timezone = timezone;
}
public String getIsp() {
return isp;
}
public void setIsp(String isp) {
this.isp = isp;
}
public String getOrg() {
return org;
}
public void setOrg(String org) {
this.org = org;
}
public String getAs() {
return as;
}
public void setAs(String as) {
this.as = as;
}
@Override
public String toString() {
return "IPAddressResponse{" +
"as='" + as + '\'' +
", org='" + org + '\'' +
", isp='" + isp + '\'' +
", timezone='" + timezone + '\'' +
", lon='" + lon + '\'' +
", lat='" + lat + '\'' +
", zip='" + zip + '\'' +
", city='" + city + '\'' +
", regionName='" + regionName + '\'' +
", region='" + region + '\'' +
", countryCode='" + countryCode + '\'' +
", country='" + country + '\'' +
", status='" + status + '\'' +
", query='" + query + '\'' +
'}';
}
}

View File

@@ -0,0 +1,28 @@
package com.serliunx.ddns.thread;
import com.serliunx.ddns.support.Assert;
import com.serliunx.ddns.support.SystemSupport;
import org.jetbrains.annotations.NotNull;
import org.slf4j.MDC;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author SerLiunx
* @since 1.0
*/
public class TaskThreadFactory implements ThreadFactory {
private final AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(@NotNull Runnable r) {
Assert.notNull(r);
Runnable runnable = () -> {
MDC.put("pid", SystemSupport.getPid());
r.run();
};
return new Thread(runnable, String.format("ddns-task-%s", count.getAndIncrement()));
}
}

View File

@@ -0,0 +1,22 @@
package com.serliunx.ddns.util;
import com.serliunx.ddns.core.instance.Instance;
import com.serliunx.ddns.support.Assert;
/**
* 实例相关工具方法集合
* @author SerLiunx
* @since 1.0
*/
public final class InstanceUtils {
private InstanceUtils(){throw new UnsupportedOperationException();}
public static void validateInstance(Instance instance){
Assert.notNull(instance);
String instanceName = instance.getName();
if(instanceName == null || instanceName.isEmpty()){
throw new NullPointerException();
}
}
}

View File

@@ -0,0 +1,86 @@
package com.serliunx.ddns.util;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
/**
* 反射相关工具类
* @author SerLiunx
* @since 1.0
*/
public final class ReflectionUtils {
private ReflectionUtils(){throw new UnsupportedOperationException();}
/**
* 获取当前类声明的所有字段
* <li> 包括父类
* @param clazz 类对象
* @param setAccessible 是否将字段的可访问性
* @return 字段列表
*/
public static Field[] getDeclaredFields(Class<?> clazz, boolean setAccessible){
if(clazz == null){
return null;
}
Field[] declaredFields = clazz.getDeclaredFields();
Field[] declaredFieldsInSuper = getDeclaredFields(clazz.getSuperclass(), setAccessible);
if(declaredFieldsInSuper != null){
Field[] newFields = new Field[declaredFields.length + declaredFieldsInSuper.length];
System.arraycopy(declaredFields, 0, newFields, 0, declaredFields.length);
System.arraycopy(declaredFieldsInSuper, 0, newFields, declaredFields.length, declaredFieldsInSuper.length);
declaredFields = newFields;
}
if(setAccessible){
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
}
}
return declaredFields;
}
/**
* 获取当前类声明的所有字段
* <li> 包括父类
* @param clazz 类对象
* @param setAccessible 是否将字段的可访问性
* @return 字段列表
*/
public static List<Field> getDeclaredFieldList(Class<?> clazz, boolean setAccessible){
return Arrays.asList(getDeclaredFields(clazz, setAccessible));
}
/**
* 复制两个对象的同名属性
* @param src 源对象
* @param dest 目标对象
* @param onlyNull 是否仅复制源对象不为空的属性
*/
public static void copyField(Object src, Object dest,boolean onlyNull){
Class<?> srcClass = src.getClass();
Class<?> destClass = dest.getClass();
List<Field> srcField = getDeclaredFieldList(srcClass, true);
List<Field> destField = getDeclaredFieldList(destClass, true);
for (Field field : destField) {
if(onlyNull){
try {
if(field.get(dest) != null){
continue;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
for (Field sf : srcField) {
if(sf.getName().equals(field.getName())){
try {
field.set(dest, sf.get(src));
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
}
}
}