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}