文章目录
- 一、写在前面
- 二、初始版本
- 1、配置转实体类工具
- 2、配置类
- 3、配置加载类
- 4、测试
- 三、扩展1:使用责任链模式提供配置加载顺序的可插拔
- 1、目标分析
- 2、处理器顶层接口
- 3、责任链
- 4、处理器实现
- 5、load方法改造
- 四、扩展2:动态扫描需要配置的类
- 1、目标分析
- 2、包扫描工具
- 3、配置注解
- 4、ClassLoader类改造
- 5、验证结果
一、写在前面
自研项目中,很多时候并没有考虑使用Spring或者Springboot框架,以至于Spring那些简化配置的神器无法使用,此时就需要自己手写一个配置加载器。
二、初始版本
1、配置转实体类工具
该工具可以实现Properties配置转换为Java实体类。
import java.lang.reflect.Method;
import java.util.Properties;
/**
* 配置工具类
*/
public class PropertiesUtils {
/**
* 配置映射到实体类
*/
public static void properties2Object(final Properties p, final Object object) {
properties2Object(p, object, "");
}
public static void properties2Object(final Properties p, final Object object, String prefix) {
Method[] methods = object.getClass().getMethods();
for (Method method : methods) {
String methodName = method.getName();
if (methodName.startsWith("set")) {
try {
// 获取配置的key
String lastName = methodName.substring(4);
String firstName = methodName.substring(3, 4);
String key = prefix + firstName.toLowerCase() + lastName;
String value = p.getProperty(key);
if (value != null) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length > 0) {
// 参数类型名
String paramName = parameterTypes[0].getSimpleName();
Object arg;
if (paramName.equals("int") || paramName.equals("Integer")) {
arg = Integer.parseInt(value);
} else if (paramName.equalsIgnoreCase("Double")) {
arg = Double.parseDouble(value);
} else if (paramName.equalsIgnoreCase("Boolean")) {
arg = Boolean.parseBoolean(value);
} else if (paramName.equalsIgnoreCase("Float")) {
arg = Float.parseFloat(value);
} else if (paramName.equalsIgnoreCase("String")) {
arg = value;
} else {
// 其他类型,根据需求 待提供
continue;
}
method.invoke(object, arg);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
2、配置类
/**
* 配置类
*/
public class Config {
// 端口
private String port = "8888";
// 注册中心
private String registerUrl = "localhost:8848";
// 服务名
private String applicationName;
// 环境
private String env = "dev";
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getRegisterUrl() {
return registerUrl;
}
public void setRegisterUrl(String registerUrl) {
this.registerUrl = registerUrl;
}
public String getApplicationName() {
return applicationName;
}
public void setApplicationName(String applicationName) {
this.applicationName = applicationName;
}
public String getEnv() {
return env;
}
public void setEnv(String env) {
this.env = env;
}
}
3、配置加载类
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Properties;
public class ConfigLoader {
// 配置文件位置
private static final String CONFIG_FILE = "myConfig.properties";
// 环境变量前缀
private static final String ENV_PREFIX = "CONFIG_";
// JVM参数前缀
private static final String JVM_PREFIX = "config.";
/**
* 单例
*/
private ConfigLoader() {
}
private static final ConfigLoader INSTANCE = new ConfigLoader();
public static ConfigLoader getInstance() {
return INSTANCE;
}
private Config config;
public static Config getConfig() {
return INSTANCE.config;
}
/**
* 加载配置:
* 优先级高的会覆盖优先级低的
* 运行参数 ->jvm参数 -> 环境变量 -> 配置文件 ->默认值
*/
public Config load(String[] args) {
// 默认值
config = new Config();
// 配置文件
loadFromConfigFile();
// 环境变量
loadFromEnv();
// jvm参数
loadFromJvm();
// 运行参数
loadFromArgs(args);
return config;
}
/**
* 从运行参数加载
*/
private void loadFromArgs(String[] args) {
// --port=1234
if(args != null && args.length > 0) {
Properties properties = new Properties();
for (String arg : args) {
if(arg.startsWith("--") && arg.contains("=")) {
properties.put(arg.substring(2, arg.indexOf("=")).trim(),
arg.substring(arg.indexOf("=") + 1).trim());
}
}
PropertiesUtils.properties2Object(properties, config);
}
}
/**
* 从JVM参数加载
*/
private void loadFromJvm() {
Properties properties = System.getProperties();
PropertiesUtils.properties2Object(properties, config);
}
/**
* 从环境变量加载
*/
private void loadFromEnv() {
Map<String, String> env = System.getenv();
Properties properties = new Properties();
properties.putAll(env);
PropertiesUtils.properties2Object(properties, config, ENV_PREFIX);
}
/**
* 从配置文件加载配置
*/
private void loadFromConfigFile() {
InputStream inputStream = ConfigLoader.class.getClassLoader().getResourceAsStream(CONFIG_FILE);
if(inputStream != null) {
Properties properties = new Properties();
try {
properties.load(inputStream);
PropertiesUtils.properties2Object(properties, config, JVM_PREFIX);
} catch (IOException e) {
System.out.println("load config file " + CONFIG_FILE + " error");
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4、测试
public class ConfigTest {
public static void main(String[] args) {
ConfigLoader.getInstance().load(args);
Config config = ConfigLoader.getConfig();
System.out.println(config.getPort());
}
}
结果:分别从默认值、配置文件、环境变量、jvm参数、java运行参数中获取配置。
三、扩展1:使用责任链模式提供配置加载顺序的可插拔
1、目标分析
当前的配置加载,依赖于load方法,当新增或者减少一个配置加载项,都需要对该方法进行修改。
我们接下来对load方法进行优化。
2、处理器顶层接口
/**
* 配置加载处理接口
*/
public interface IConfigLoadHandler {
/**
* 对配置进行加载
*/
void handle(Config config);
}
3、责任链
public class ConfigLoadHandlerChain {
private List<IConfigLoadHandler> handlers = new ArrayList<>();
public static ConfigLoadHandlerChain getInstance() {
return new ConfigLoadHandlerChain();
}
public ConfigLoadHandlerChain addHandler(IConfigLoadHandler handler) {
this.handlers.add(handler);
return this;
}
public void handle(Config config) {
for (IConfigLoadHandler handler : handlers) {
handler.handle(config);
}
}
}
4、处理器实现
/**
* 从启动参数加载
*/
public class ConfigLoadFromArgs implements IConfigLoadHandler {
@Override
public void handle(Config config) {
// --port=1234
String[] args = ConfigLoader.getArgs();
if(args != null && args.length > 0) {
Properties properties = new Properties();
for (String arg : args) {
if(arg.startsWith("--") && arg.contains("=")) {
properties.put(arg.substring(2, arg.indexOf("=")).trim(),
arg.substring(arg.indexOf("=") + 1).trim());
}
}
PropertiesUtils.properties2Object(properties, config);
}
}
}
/**
* 从环境变量加载
*/
public class ConfigLoadFromEnv implements IConfigLoadHandler {
// 环境变量前缀
private static final String ENV_PREFIX = "CONFIG_";
@Override
public void handle(Config config) {
Map<String, String> env = System.getenv();
Properties properties = new Properties();
properties.putAll(env);
PropertiesUtils.properties2Object(properties, config, ENV_PREFIX);
}
}
/**
* 从配置文件加载
*/
public class ConfigLoadFromFile implements IConfigLoadHandler {
// 配置文件位置
private static final String CONFIG_FILE = "myConfig.properties";
@Override
public void handle(Config config) {
InputStream inputStream = ConfigLoader.class.getClassLoader().getResourceAsStream(CONFIG_FILE);
if(inputStream != null) {
Properties properties = new Properties();
try {
properties.load(inputStream);
PropertiesUtils.properties2Object(properties, config);
} catch (IOException e) {
System.out.println("load config file " + CONFIG_FILE + " error");
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 从JVM参数加载
*/
public class ConfigLoadFromJvm implements IConfigLoadHandler {
// 环境变量前缀
private static final String JVM_PREFIX = "config.";
@Override
public void handle(Config config) {
Properties properties = System.getProperties();
PropertiesUtils.properties2Object(properties, config, JVM_PREFIX);
}
}
5、load方法改造
/**
* 加载配置:
* 优先级高的会覆盖优先级低的
* 运行参数 ->jvm参数 -> 环境变量 -> 配置文件 ->默认值
*/
public Config load(String[] args) {
ConfigLoader.args = args;
// 默认值
config = new Config();
ConfigLoadHandlerChain configLoadHandlerChain =
ConfigLoadHandlerChain.getInstance()
.addHandler(new ConfigLoadFromFile())// 配置文件
.addHandler(new ConfigLoadFromEnv())// 环境变量
.addHandler(new ConfigLoadFromJvm())// jvm参数
.addHandler(new ConfigLoadFromArgs());// 运行参数
configLoadHandlerChain.handle(config);
return config;
}
此时,我们扩展配置加载处理器,只需要定义一个处理器实现类,实现IConfigLoadHandler接口即可,然后在load方法中可以实现可插拔。
四、扩展2:动态扫描需要配置的类
1、目标分析
之前我们加载的配置,只能加载到固定的一个Config类中,而且跟配置的加载实现类有高度的耦合性。
我们想要实现一个,类似spring一样的可以将配置类分开,比如说一个配置类管着启动参数,一个配置类管着应用参数。
2、包扫描工具
import java.io.File;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* 包扫描工具类
*/
public class PackageScanner {
private static final String CLASS_FILE_SUFFIX = ".class";
public static List<Class<?>> scanPackage(String packageName, Class<? extends Annotation> annotation) {
List<Class<?>> classes = new ArrayList<>();
String packagePath = packageName.replace('.', '/');
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL packageURL = classLoader.getResource(packagePath);
if (packageURL != null) {
File packageDirectory = new File(packageURL.getFile());
if (packageDirectory.exists()) {
scanFiles(packageName, packageDirectory, annotation, classes);
}
}
return classes;
}
private static void scanFiles(String packageName, File directory, Class<? extends Annotation> annotation, List<Class<?>> classes) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(CLASS_FILE_SUFFIX)) {
try {
// 获取文件名(去除后缀)
String className = packageName + "." + file.getName().substring(0, file.getName().length() - CLASS_FILE_SUFFIX.length());
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(annotation)) {
classes.add(clazz);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else if (file.isDirectory()) {
// 递归扫描子目录
String subPackageName = packageName + "." + file.getName();
scanFiles(subPackageName, file, annotation, classes);
}
}
}
}
// public static void main(String[] args) {
// List<Class<?>> classes = scanPackage("com.example", Config.class);
// for (Class<?> clazz : classes) {
// System.out.println(clazz.getName());
// }
// }
}
3、配置注解
标注该注解的类,都会被自动加载到配置中。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 配置类标注
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {
}
4、ClassLoader类改造
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ConfigLoader {
// 启动参数
private static String[] args;
public static String[] getArgs() {
return args;
}
private static Map<Class, Object> configMap = new HashMap<>();
/**
* 单例
*/
private ConfigLoader() {
// 扫描该类下所有标注@Config的类
List<Class<?>> classes = PackageScanner.scanPackage(ConfigLoader.class.getPackage().getName(), Config.class);
for (Class<?> clazz : classes) {
try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object o = constructor.newInstance();
configMap.put(clazz, o);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
private static final ConfigLoader INSTANCE = new ConfigLoader();
public static ConfigLoader getInstance() {
return INSTANCE;
}
/**
* 加载配置:
* 优先级高的会覆盖优先级低的
* 运行参数 ->jvm参数 -> 环境变量 -> 配置文件 ->默认值
*/
public void load(String[] args) {
ConfigLoader.args = args;
ConfigLoadHandlerChain configLoadHandlerChain =
ConfigLoadHandlerChain.getInstance()
.addHandler(new ConfigLoadFromFile())// 配置文件
.addHandler(new ConfigLoadFromEnv())// 环境变量
.addHandler(new ConfigLoadFromJvm())// jvm参数
.addHandler(new ConfigLoadFromArgs());// 运行参数
// 加载所有配置
configMap.forEach((k, v) -> {
configLoadHandlerChain.handle(v);
});
}
public static <T> T getConfig(Class<T> c) {
Object o = configMap.get(c);
return o == null ? null : (T) o;
}
}
5、验证结果
ApplicationConfig和UserConfig使用@Config标注,我们写一下测试类:
public class ConfigTest {
public static void main(String[] args) {
ConfigLoader.getInstance().load(args);
ApplicationConfig config = ConfigLoader.getConfig(ApplicationConfig.class);
System.out.println(config.getPort());
UserConfig config2 = ConfigLoader.getConfig(UserConfig.class);
System.out.println(config2.getRegisterUrl());
}
}
后续再创建配置类的话,直接用@Config标注即可自动实现配置加载。