Java Refactoring Practice: Implement a configuration loader and use design patterns to optimize it

1. Write in front

In self-research projects, the use of Spring or Springboot framework is often not considered, so that Spring’s artifacts that simplify configuration cannot be used. In this case, you need to write a configuration loader by yourself.

2. Initial version

1. Configuration to entity class tool

This tool can convert Properties configuration into Java entity classes.

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. Configuration class

/**
 * 配置类
 */
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. Configure loading class

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. Test

public class ConfigTest {
    
    

    public static void main(String[] args) {
    
    
        ConfigLoader.getInstance().load(args);
        Config config = ConfigLoader.getConfig();
        System.out.println(config.getPort());
    }
}

Result: Obtain configurations from default values, configuration files, environment variables, jvm parameters, and java running parameters.

3. Extension 1: Use the responsibility chain model to provide pluggable configuration loading sequence

1. Goal analysis

The current configuration loading depends on the load method. When a configuration add-on is added or deleted, this method needs to be modified.

We will optimize the load method next.

2. Processor top-level interface

/**
 * 配置加载处理接口
 */
public interface IConfigLoadHandler {
    
    

    /**
     * 对配置进行加载
     */
    void handle(Config config);

}

3. Chain of responsibility

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. Processor implementation

/**
 * 从启动参数加载
 */
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. Modification of load method

    /**
     * 加载配置:
     * 优先级高的会覆盖优先级低的
     * 运行参数 ->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;
    }

At this time, when we extend the configuration loading processor, we only need to define a processor implementation class and implement the IConfigLoadHandler interface, and then implement pluggability in the load method.

4. Extension 2: Dynamic scanning of classes that need to be configured

1. Goal analysis

The configuration we loaded before can only be loaded into a fixed Config class, and it is highly coupled with the configuration loading implementation class.

We want to implement a configuration class that can be separated like spring. For example, one configuration class manages startup parameters and another configuration class manages application parameters.

2. Package scanning tool

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. Configuration annotations

Classes marked with this annotation will be automatically loaded into the configuration.

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 class modification

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. Verification results

ApplicationConfig and UserConfig are annotated with @Config. Let’s write a test class:

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

If you create a configuration class later, you can directly annotate it with @Config to automatically load the configuration.

Guess you like

Origin blog.csdn.net/A_art_xiang/article/details/132968406
Recommended