如何在Java EE和Spring Boot中热重新加载属性?
我想到了许多内部解决方案.就像在数据库中拥有属性并每隔N秒轮询它一样.然后还要检查.properties文件的时间戳修改并重新加载它.
但我正在研究Java EE标准和Spring引导文档,我似乎无法找到一些最好的方法. 我需要我的应用程序来读取属性文件(或环境变量或数据库参数),然后才能重新读取它们.生产中使用的最佳实践是什么? 一个正确的答案至少可以解决一个场景(Spring Boot或Java EE)并提供一个关于如何使其在另一个场景上工作的概念线索 解决方法
经过进一步的研究,reloading properties must be carefully considered.例如,在Spring中,我们可以重新加载属性的“当前”值而没有太多问题.但.在上下文初始化时根据application.properties文件中存在的值(例如,数据源,连接池,队列等)初始化资源时,必须特别小心.
注意: 用于Spring和Java EE的抽象类不是清洁代码的最佳示例.但它易于使用,它确实满足了这个基本的初始要求: >不使用Java 8 Classes以外的外部库. 对于Spring Boot 此代码有助于在不使用Spring Cloud Config服务器的情况下热重新加载application.properties文件(对于某些用例可能过度杀毒) 这个抽象类你可以复制&粘贴(SO好吃的东西:D)这是code derived from this SO answer // imports from java/spring/lombok public abstract class ReloadableProperties { @Autowired protected StandardEnvironment environment; private long lastModTime = 0L; private Path configPath = null; private PropertySource<?> appConfigPropertySource = null; @PostConstruct private void stopIfProblemsCreatingContext() { System.out.println("reloading"); MutablePropertySources propertySources = environment.getPropertySources(); Optional<PropertySource<?>> appConfigPsOp = StreamSupport.stream(propertySources.spliterator(),false) .filter(ps -> ps.getName().matches("^.*applicationConfig.*file:.*$")) .findFirst(); if (!appConfigPsOp.isPresent()) { // this will stop context initialization // (i.e. kill the spring boot program before it initializes) throw new RuntimeException("Unable to find property Source as file"); } appConfigPropertySource = appConfigPsOp.get(); String filename = appConfigPropertySource.getName(); filename = filename .replace("applicationConfig: [file:","") .replaceAll("]$",""); configPath = Paths.get(filename); } @Scheduled(fixedRate=2000) private void reload() throws IOException { System.out.println("reloading..."); long currentModTs = Files.getLastModifiedTime(configPath).toMillis(); if (currentModTs > lastModTime) { lastModTime = currentModTs; Properties properties = new Properties(); @Cleanup InputStream inputStream = Files.newInputStream(configPath); properties.load(inputStream); environment.getPropertySources() .replace( appConfigPropertySource.getName(),new PropertiesPropertySource( appConfigPropertySource.getName(),properties ) ); System.out.println("Reloaded."); propertiesReloaded(); } } protected abstract void propertiesReloaded(); } 然后创建一个bean类,允许从使用抽象类的applicatoin.properties中检索属性值 @Component public class AppProperties extends ReloadableProperties { public String dynamicProperty() { return environment.getProperty("dynamic.prop"); } public String anotherDynamicProperty() { return environment.getProperty("another.dynamic.prop"); } @Override protected void propertiesReloaded() { // do something after a change in property values was done } } 确保将@EnableScheduling添加到@SpringBootApplication中 @SpringBootApplication @EnableScheduling public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class,args); } } 现在,您可以在任何需要的地方自动连接AppProperties Bean.只需确保始终调用其中的方法,而不是将其值保存在变量中.并确保重新配置使用可能不同的属性值初始化的任何资源或bean. 目前,我只使用外部和默认找到的./config/application.properties文件对此进行了测试. 对于Java EE 我做了一个普通的Java SE抽象类来完成这项工作. 你可以复制&粘贴这个: // imports from java.* and javax.crypto.* public abstract class ReloadableProperties { private volatile Properties properties = null; private volatile String propertiesPassword = null; private volatile long lastModTimeOfFile = 0L; private volatile long lastTimeChecked = 0L; private volatile Path propertyFileAddress; abstract protected void propertiesUpdated(); public class DynProp { private final String propertyName; public DynProp(String propertyName) { this.propertyName = propertyName; } public String val() { try { return ReloadableProperties.this.getString(propertyName); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } } protected void init(Path path) { this.propertyFileAddress = path; initOrReloadIfNeeded(); } private synchronized void initOrReloadIfNeeded() { boolean firstTime = lastModTimeOfFile == 0L; long currentTs = System.currentTimeMillis(); if ((lastTimeChecked + 3000) > currentTs) return; try { File fa = propertyFileAddress.toFile(); long currModTime = fa.lastModified(); if (currModTime > lastModTimeOfFile) { lastModTimeOfFile = currModTime; InputStreamReader isr = new InputStreamReader(new FileInputStream(fa),StandardCharsets.UTF_8); Properties prop = new Properties(); prop.load(isr); properties = prop; isr.close(); File passwordFiles = new File(fa.getAbsolutePath() + ".key"); if (passwordFiles.exists()) { byte[] bytes = Files.readAllBytes(passwordFiles.toPath()); propertiesPassword = new String(bytes,StandardCharsets.US_ASCII); propertiesPassword = propertiesPassword.trim(); propertiesPassword = propertiesPassword.replaceAll("(r|n)",""); } } updateProperties(); if (!firstTime) propertiesUpdated(); } catch (Exception e) { e.printStackTrace(); } } private void updateProperties() { List<DynProp> dynProps = Arrays.asList(this.getClass().getDeclaredFields()) .stream() .filter(f -> f.getType().isAssignableFrom(DynProp.class)) .map(f-> fromField(f)) .collect(Collectors.toList()); for (DynProp dp :dynProps) { if (!properties.containsKey(dp.propertyName)) { System.out.println("propertyName: "+ dp.propertyName + " does not exist in property file"); } } for (Object key : properties.keySet()) { if (!dynProps.stream().anyMatch(dp->dp.propertyName.equals(key.toString()))) { System.out.println("property in file is not used in application: "+ key); } } } private DynProp fromField(Field f) { try { return (DynProp) f.get(this); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } protected String getString(String param) throws Exception { initOrReloadIfNeeded(); String value = properties.getProperty(param); if (value.startsWith("ENC(")) { String cipheredText = value .replace("ENC(","") .replaceAll(")$",""); value = decrypt(cipheredText,propertiesPassword); } return value; } public static String encrypt(String plainText,String key) throws NoSuchPaddingException,NoSuchAlgorithmException,InvalidAlgorithmParameterException,InvalidKeyException,BadPaddingException,IllegalBlockSizeException,InvalidKeySpecException { SecureRandom secureRandom = new SecureRandom(); byte[] keyBytes = key.getBytes(StandardCharsets.US_ASCII); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(key.toCharArray(),new byte[]{0,1,2,3,4,5,6,7},65536,128); SecretKey tmp = factory.generateSecret(spec); SecretKey secretKey = new SecretKeySpec(tmp.getEncoded(),"AES"); byte[] iv = new byte[12]; secureRandom.nextBytes(iv); final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec parameterSpec = new GCMParameterSpec(128,iv); //128 bit auth tag length cipher.init(Cipher.ENCRYPT_MODE,secretKey,parameterSpec); byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); ByteBuffer byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length); byteBuffer.putInt(iv.length); byteBuffer.put(iv); byteBuffer.put(cipherText); byte[] cipherMessage = byteBuffer.array(); String cyphertext = Base64.getEncoder().encodeToString(cipherMessage); return cyphertext; } public static String decrypt(String cypherText,InvalidKeySpecException { byte[] cipherMessage = Base64.getDecoder().decode(cypherText); ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage); int ivLength = byteBuffer.getInt(); if(ivLength < 12 || ivLength >= 16) { // check input parameter throw new IllegalArgumentException("invalid iv length"); } byte[] iv = new byte[ivLength]; byteBuffer.get(iv); byte[] cipherText = new byte[byteBuffer.remaining()]; byteBuffer.get(cipherText); byte[] keyBytes = key.getBytes(StandardCharsets.US_ASCII); final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(key.toCharArray(),"AES"); cipher.init(Cipher.DECRYPT_MODE,new GCMParameterSpec(128,iv)); byte[] plainText= cipher.doFinal(cipherText); String plain = new String(plainText,StandardCharsets.UTF_8); return plain; } } 然后你可以这样使用它: public class AppProperties extends ReloadableProperties { public static final AppProperties INSTANCE; static { INSTANCE = new AppProperties(); INSTANCE.init(Paths.get("application.properties")); } @Override protected void propertiesUpdated() { // run code every time a property is updated } public final DynProp wsUrl = new DynProp("ws.url"); public final DynProp hiddenText = new DynProp("hidden.text"); } 如果您想使用编码属性,您可以将其值包含在ENC()中,并且将在具有添加的.key扩展名的属性文件的相同路径和名称中搜索用于解密的密码.在此示例中,它将在application.properties.key文件中查找密码. application.properties – > ws.url=http://some webside hidden.text=ENC(AAAADCzaasd9g61MI4l5sbCXrFNaQfQrgkxygNmFa3UuB9Y+YzRuBGYj+A==) aplication.properties.key – > password aca 为了加密Java EE解决方案的属性值,我在Symmetric Encryption with AES in Java and Android上查阅了Patrick Favre-Bulle的优秀文章.然后在这个SO问题中检查了密码,块模式和填充,关于AES/GCM/NoPadding.最后我将AES位从密码中导出来自@erickson关于AES Password Based Encryption的优秀答案.关于Spring中值属性的加密,我认为它们与Java Simplified Encryption集成在一起 这个有资格作为最佳实践的资格可能超出范围.这个答案显示了如何在Spring Boot和Java EE中使用可重新加载的属性. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |