001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.jboot.app.config;
017
018import com.google.common.collect.ArrayListMultimap;
019import com.google.common.collect.Multimap;
020import io.jboot.app.config.annotation.ConfigModel;
021import io.jboot.app.config.support.apollo.ApolloConfigManager;
022import io.jboot.app.config.support.nacos.NacosConfigManager;
023
024import java.io.File;
025import java.lang.reflect.Method;
026import java.util.*;
027import java.util.concurrent.ConcurrentHashMap;
028
029/**
030 * 配置管理类
031 */
032public class JbootConfigManager {
033
034    //启动参数
035    private static Map<String, String> argMap;
036
037    //jboot.properties 和 jboot-dev.properties 等内容
038    private Properties mainProperties;
039
040    //分布式配置
041    private Map remoteProperties;
042
043    //ConfigObje 缓存 class + prefix : object
044    private Map<String, Object> configCache = new ConcurrentHashMap<>();
045
046    //监听器
047    private Multimap<String, JbootConfigChangeListener> listenersMultimap = ArrayListMultimap.create();
048
049    //配置内容解密工具
050    private JbootConfigDecryptor decryptor;
051
052
053    private static JbootConfigManager instance;
054
055    public static JbootConfigManager me() {
056        if (instance == null) {
057            instance = new JbootConfigManager();
058        }
059        return instance;
060    }
061
062    private JbootConfigManager() {
063        init();
064    }
065
066
067    private void init() {
068        String fileName = getConfigValue(null, "jboot_properties_name");
069        if (fileName == null || fileName.length() == 0) {
070            fileName = "jboot";
071        }
072
073        String pathName = getConfigValue(null, "jboot_properties_path");
074        mainProperties = JbootConfigKit.readProperties(pathName, fileName);
075
076
077        //可以直接在 默认目录下的 jboot.properties 再次指定外部目录
078        String newFileName = getConfigValue(mainProperties, "jboot_properties_name");
079        if (newFileName != null && newFileName.length() > 0 && "jboot".equals(fileName)) {
080            fileName = newFileName;
081        }
082
083        String newPathName = getConfigValue(mainProperties, "jboot_properties_path");
084        if (newPathName != null && newPathName.length() > 0 && (pathName == null || pathName.length() == 0)) {
085            pathName = newPathName;
086        }
087
088
089        //配置了 pathName,需要再去 path 读取 jboot.properties 文件
090        if (pathName != null && pathName.length() > 0) {
091            Properties newMainProperties = JbootConfigKit.readProperties(pathName, fileName);
092            mainProperties.putAll(newMainProperties);
093        }
094
095
096        String mode = getConfigValue("jboot.app.mode");
097        if (JbootConfigKit.isNotBlank(mode)) {
098            //开始加载 mode properties
099            //并全部添加覆盖掉掉 main properties
100            String modeFileName = fileName + "-" + mode;
101            Properties modeProperties = JbootConfigKit.readProperties(pathName, modeFileName);
102
103            mainProperties.putAll(modeProperties);
104        }
105
106
107        //通过启动参数 --config=./xxx.properties 来指定配置文件启动
108        String configFile = getConfigValue(null, "config");
109        Properties configFileProperties = null;
110        if (configFile != null && configFile.startsWith("./")) {
111            configFileProperties = JbootConfigKit.readExternalProperties(configFile.substring(2));
112        } else if (configFile != null) {
113            configFileProperties = JbootConfigKit.readPropertiesFile(new File(configFile));
114        } else {
115            //通过在 fatjar 的相同目录下,创建 jboot.properties 配置文件来启动
116            configFileProperties = JbootConfigKit.readExternalProperties(fileName);
117        }
118
119        if (configFileProperties != null && !configFileProperties.isEmpty()) {
120            mainProperties.putAll(configFileProperties);
121        }
122
123
124        NacosConfigManager.me().init(this);
125        ApolloConfigManager.me().init(this);
126    }
127
128
129    public JbootConfigDecryptor getDecryptor() {
130        return decryptor;
131    }
132
133    public void setDecryptor(JbootConfigDecryptor decryptor) {
134        this.decryptor = decryptor;
135    }
136
137    public <T> T get(Class<T> clazz) {
138        ConfigModel configModel = clazz.getAnnotation(ConfigModel.class);
139        if (configModel == null) {
140            return get(clazz, null, null);
141        }
142        return get(clazz, configModel.prefix(), configModel.file());
143    }
144
145
146    /**
147     * 获取配置信息,并创建和赋值clazz实例
148     *
149     * @param clazz  指定的类
150     * @param prefix 配置文件前缀
151     * @param <T>
152     * @return
153     */
154    public <T> T get(Class<T> clazz, String prefix, String file) {
155
156        /**
157         * 开发模式下,热加载会导致由于 Config 是不同的 ClassLoader 而导致异常,
158         * 如果走缓存会Class转化异常
159         */
160        if (isDevMode()) {
161            return createConfigObject(clazz, prefix, file);
162        }
163
164        Object configObject = configCache.get(clazz.getName() + prefix);
165
166        if (configObject == null) {
167            Object obj = createConfigObject(clazz, prefix, file);
168            configCache.putIfAbsent(clazz.getName() + prefix, obj);
169            configObject = configCache.get(clazz.getName() + prefix);
170        }
171
172        return (T) configObject;
173    }
174
175
176    /**
177     * 刷新数据,并返回新的数据
178     *
179     * @param clazz
180     * @param <T>
181     * @return
182     */
183    public <T> T refreshAndGet(Class<T> clazz) {
184        ConfigModel configModel = clazz.getAnnotation(ConfigModel.class);
185        if (configModel == null) {
186            return refreshAndGet(clazz, null, null);
187        }
188        return refreshAndGet(clazz, configModel.prefix(), configModel.file());
189    }
190
191
192    /**
193     * 刷新数据,并返回新的数据
194     *
195     * @param clazz
196     * @param prefix
197     * @param file
198     * @param <T>
199     * @return
200     */
201    public <T> T refreshAndGet(Class<T> clazz, String prefix, String file) {
202
203        configCache.remove(clazz.getName() + prefix);
204        refreshMainProperties();
205
206        return get(clazz, prefix, file);
207    }
208
209
210    private void refreshMainProperties() {
211
212        mainProperties.putAll(JbootConfigKit.readProperties("jboot"));
213
214        String mode = getConfigValue(mainProperties, "jboot.app.mode");
215        if (JbootConfigKit.isNotBlank(mode)) {
216            String modeFileName = "jboot-" + mode;
217            mainProperties.putAll(JbootConfigKit.readProperties(modeFileName));
218        }
219    }
220
221
222    /**
223     * 创建一个新的配置对象(Object)
224     *
225     * @param clazz
226     * @param prefix
227     * @param file
228     * @param <T>
229     * @return
230     */
231    public synchronized <T> T createConfigObject(Class<T> clazz, String prefix, String file) {
232        T configObject = JbootConfigKit.newInstance(clazz);
233        for (Method setterMethod : JbootConfigKit.getClassSetMethods(clazz)) {
234            String key = buildKey(prefix, setterMethod);
235            String value = getConfigValue(key);
236
237            if (JbootConfigKit.isNotBlank(file)) {
238                JbootProp prop = new JbootProp(file);
239                String filePropValue = getConfigValue(prop.getProperties(), key);
240                if (JbootConfigKit.isNotBlank(filePropValue)) {
241                    value = filePropValue;
242                }
243            }
244
245            if (JbootConfigKit.isNotBlank(value)) {
246                Object val = JbootConfigKit.convert(setterMethod.getParameterTypes()[0], value, setterMethod.getGenericParameterTypes()[0]);
247                if (val != null) {
248                    try {
249                        setterMethod.invoke(configObject, val);
250                    } catch (Exception e) {
251                        e.printStackTrace();
252                    }
253                }
254            }
255        }
256
257        return configObject;
258    }
259
260
261    private String buildKey(String prefix, Method method) {
262        String key = JbootConfigKit.firstCharToLowerCase(method.getName().substring(3));
263        if (JbootConfigKit.isNotBlank(prefix)) {
264            key = prefix.trim() + "." + key;
265        }
266        return key;
267    }
268
269
270    public String getConfigValue(String key) {
271        return getConfigValue(mainProperties, key);
272    }
273
274
275    public String getConfigValue(Properties properties, String key) {
276        if (JbootConfigKit.isBlank(key)) {
277            return null;
278        }
279        String originalValue = getOriginalConfigValue(properties, key);
280        String decryptValue = decryptor != null ? decryptor.decrypt(key, originalValue) : originalValue;
281        String parseValue = JbootConfigKit.parseValue(this, decryptValue);
282        return parseValue == null || parseValue.trim().length() == 0 ? null : parseValue.trim();
283    }
284
285
286    /**
287     * 获取值的优先顺序:1、远程配置  2、启动配置   3、环境变量  4、系统属性  5、properties配置文件
288     *
289     * @param key
290     * @return
291     */
292    private String getOriginalConfigValue(Properties properties, String key) {
293
294        String value = null;
295
296        //优先读取分布式配置内容
297        if (remoteProperties != null) {
298            value = (String) remoteProperties.get(key);
299            if (JbootConfigKit.isNotBlank(value)) {
300                return value.trim();
301            }
302        }
303
304        //boot arg
305        value = getBootArg(key);
306        if (JbootConfigKit.isNotBlank(value)) {
307            return value.trim();
308        }
309
310        //env
311        value = System.getenv(key);
312        if (JbootConfigKit.isNotBlank(value)) {
313            return value.trim();
314        }
315
316        //upperCase env
317        // 把xxx.xxx.xxx 转换为 XXX_XXX_XXX,
318        // 例如:jboot.datasource.url 转换为 JBOOT_DATASOURCE_URL
319        String tempKey = key.toUpperCase().replace('.', '_');
320        value = System.getenv(tempKey);
321        if (JbootConfigKit.isNotBlank(value)) {
322            return value.trim();
323        }
324
325        //system property
326        value = System.getProperty(key);
327        if (JbootConfigKit.isNotBlank(value)) {
328            return value.trim();
329        }
330
331        //user properties
332        if (properties != null) {
333            value = (String) properties.get(key);
334            if (JbootConfigKit.isNotBlank(value)) {
335                return value.trim();
336            }
337        }
338
339        return null;
340    }
341
342    /**
343     * 获取Jboot默认的配置信息
344     *
345     * @return
346     */
347    public Properties getProperties() {
348
349        Properties properties = new Properties();
350        properties.putAll(mainProperties);
351
352        if (System.getenv() != null) {
353            for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
354                properties.put(entry.getKey(), entry.getValue());
355            }
356        }
357
358
359        if (System.getProperties() != null) {
360            properties.putAll(System.getProperties());
361        }
362
363
364        if (getBootArgs() != null) {
365            for (Map.Entry<String, String> entry : getBootArgs().entrySet()) {
366                properties.put(entry.getKey(), entry.getValue());
367            }
368        }
369
370        if (remoteProperties != null) {
371            properties.putAll(remoteProperties);
372        }
373
374        return properties;
375    }
376
377    public Map<String, Object> getConfigCache() {
378        return configCache;
379    }
380
381
382    public synchronized void setRemoteProperty(String key, String value) {
383        if (remoteProperties == null) {
384            remoteProperties = new ConcurrentHashMap();
385        }
386        remoteProperties.put(key, value);
387    }
388
389
390    public void removeRemoteProperty(String key) {
391        if (remoteProperties != null) {
392            remoteProperties.remove(key);
393        }
394    }
395
396
397    public synchronized void setRemoteProperties(Map map) {
398        if (remoteProperties == null) {
399            remoteProperties = new ConcurrentHashMap();
400        }
401        remoteProperties.putAll(map);
402    }
403
404
405    public void addConfigChangeListener(JbootConfigChangeListener listener, Class forClass) {
406        ConfigModel configModel = (ConfigModel) forClass.getAnnotation(ConfigModel.class);
407        if (configModel == null) {
408            throw new IllegalArgumentException("forClass:" + forClass + " has no @ConfigModel annotation");
409        }
410
411        String prefix = configModel.prefix();
412        List<Method> setterMethods = JbootConfigKit.getClassSetMethods(forClass);
413        if (setterMethods != null) {
414            for (Method setterMethod : setterMethods) {
415                String key = buildKey(prefix, setterMethod);
416                listenersMultimap.put(key, listener);
417            }
418        }
419    }
420
421
422    public void addConfigChangeListener(JbootConfigChangeListener listener, String... forKeys) {
423        if (listener == null) {
424            throw new NullPointerException("listener must not null.");
425        }
426
427
428        if (forKeys == null || forKeys.length == 0) {
429            throw new NullPointerException("forKeys must not null or empty.");
430        }
431
432        for (String forKey : forKeys) {
433            listenersMultimap.put(forKey, listener);
434        }
435    }
436
437
438    public void removeConfigChangeListener(JbootConfigChangeListener listener) {
439        listenersMultimap.entries().removeIf(entry -> entry.getValue() == listener);
440    }
441
442
443    public void notifyChangeListeners(String key, String newValue, String oldValue) {
444        if (key == null) {
445            return;
446        }
447
448        Collection<JbootConfigChangeListener> listeners = listenersMultimap.get(key);
449        for (JbootConfigChangeListener listener : listeners) {
450            try {
451                listener.onChange(key, newValue, oldValue);
452            } catch (Throwable ex) {
453                com.jfinal.kit.LogKit.error(ex.toString(), ex);
454            }
455        }
456    }
457
458
459    /**
460     * 解析启动参数
461     *
462     * @param args
463     */
464    public static void parseArgs(String[] args) {
465        if (args == null || args.length == 0) {
466            return;
467        }
468
469        for (String arg : args) {
470            int indexOf = arg.indexOf("=");
471            if (arg.startsWith("--") && indexOf > 0) {
472                String key = arg.substring(2, indexOf);
473                String value = arg.substring(indexOf + 1);
474                setBootArg(key, value);
475            }
476        }
477    }
478
479    public static void setBootArg(String key, Object value) {
480        if (argMap == null) {
481            argMap = new HashMap<>();
482        }
483        if (key == null) {
484            return;
485        }
486        if (value == null || value.toString().trim().length() == 0) {
487            argMap.remove(key.trim());
488        } else {
489            argMap.put(key.trim(), value.toString().trim());
490        }
491    }
492
493    public static void setBootProperties(Properties properties) {
494        Objects.requireNonNull(properties, "properties must not be null");
495        properties.forEach((o, o2) -> setBootArg(o.toString(), o2));
496    }
497
498
499    public static void setBootProperties(String propertiesFilePath) {
500        File file = new File(propertiesFilePath);
501        if (file.exists()) {
502            setBootProperties(file);
503        } else {
504            setBootProperties(new JbootProp(propertiesFilePath).getProperties());
505        }
506    }
507
508
509    public static void setBootProperties(File propertiesFile) {
510        if (propertiesFile.exists()) {
511            setBootProperties(new JbootProp(propertiesFile).getProperties());
512        } else {
513            System.err.println("Warning: properties file not exists: " + propertiesFile);
514        }
515    }
516
517    /**
518     * 获取启动参数
519     *
520     * @param key
521     * @return
522     */
523    public String getBootArg(String key) {
524        if (argMap == null) {
525            return null;
526        }
527        return argMap.get(key);
528    }
529
530    public Map<String, String> getBootArgs() {
531        return argMap;
532    }
533
534
535    private Boolean devMode = null;
536
537    public boolean isDevMode() {
538        if (devMode == null) {
539            String appMode = getConfigValue("jboot.app.mode");
540            devMode = (null == appMode || "".equals(appMode.trim()) || "dev".equalsIgnoreCase(appMode.trim()));
541        }
542        return devMode;
543    }
544
545    public void setDevMode(Boolean devMode) {
546        this.devMode = devMode;
547    }
548}