如何在 Java EE 和 Spring Boot 中热重载属性?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/52594764/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 00:32:02  来源:igfitidea点击:

How to hot-reload properties in Java EE and Spring Boot?

javaspringspring-bootjakarta-eeproperties

提问by David Hofmann

Many in-house solutions come to mind. Like having the properties in a database and poll it every N secs. Then also check the timestamp modification for a .properties file and reload it.

许多内部解决方案浮现在脑海中。就像在数据库中拥有属性并每 N 秒轮询一次一样。然后还要检查 .properties 文件的时间戳修改并重新加载它。

But I was looking in Java EE standards and spring boot docs and I can't seem to find some best way of doing it.

但是我正在查看 Java EE 标准和 Spring Boot 文档,但似乎找不到最好的方法。

I need my application to read a properties file(or env. variables or DB parameters), then be able to re-read them. What is the best practice being used in production?

我需要我的应用程序读取属性文件(或环境变量或数据库参数),然后才能重新读取它们。生产中使用的最佳实践是什么?

A correct answer will at least solve one scenario (Spring Boot or Java EE) and provide a conceptual clue on how to make it work on the other

正确的答案至少可以解决一个场景(Spring Boot 或 Java EE),并提供有关如何使其在另一个场景中工作的概念性线索

采纳答案by David Hofmann

After further research, reloading properties must be carefully considered. In Spring, for example, we can reload the 'current' values of properties without much problem. But. Special care must be taken when resources were initialized at the context initialization time based on the values that were present in the application.properties file (e.g. Datasources, connection pools, queues, etc.).

经过进一步研究,必须仔细考虑重装属性。例如,在 Spring 中,我们可以毫无问题地重新加载属性的“当前”值。但。在上下文初始化时根据 application.properties 文件中存在的值(例如数据源、连接池、队列等)初始化资源时,必须特别小心。

NOTE:

注意

The abstract classes used for Spring and Java EE are not the best example of clean code. But it is easy to use and it does address this basic initial requirements:

用于 Spring 和 Java EE 的抽象类并不是干净代码的最佳示例。但它易于使用,并且确实满足了以下基本的初始要求:

  • No usage of external libraries other than Java 8 Classes.
  • Only one file to solve the problem (~160 lines for the Java EE version).
  • Usage of standard Java Properties UTF-8 encoded file available in the File System.
  • Support encrypted properties.
  • 不使用 Java 8 类以外的外部库。
  • 只需一个文件即可解决问题(Java EE 版本约 160 行)。
  • 使用文件系统中可用的标准 Java 属性 UTF-8 编码文件。
  • 支持加密属性。

For Spring Boot

对于 Spring Boot

This code helps with hot-reloading application.properties file without the usage of a Spring Cloud Config server (which may be overkill for some use cases)

此代码有助于在不使用 Spring Cloud Config 服务器的情况下热重载 application.properties 文件(这对于某些用例可能有点过分)

This abstract class you may just copy & paste (SO goodies :D ) It's a code derived from this SO answer

这个抽象类你可以复制和粘贴(SO goodies :D)这是从这个 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();
}

Then you make a bean class that allows retrieval of property values from applicatoin.properties that uses the abstract class

然后创建一个 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
    }
}

Make sure to add @EnableScheduling to your @SpringBootApplication

确保将 @EnableScheduling 添加到您的 @SpringBootApplication

@SpringBootApplication
@EnableScheduling
public class MainApp  {
   public static void main(String[] args) {
      SpringApplication.run(MainApp.class, args);
   }
}

Now you can auto-wirethe AppProperties Bean wherever you need it. Just make sure to alwayscall the methods in it instead of saving it's value in a variable. And make sure to re-configure any resource or bean that was initialized with potentially different property values.

现在您可以在任何需要的地方自动连接 AppProperties Bean。只需确保始终调用其中的方法,而不是将其值保存在变量中。并确保重新配置任何使用可能不同的属性值初始化的资源或 bean。

For now, I have only tested this with an external-and-default-found ./config/application.propertiesfile.

目前,我仅使用外部和默认找到的./config/application.properties文件对此进行了测试。

For Java EE

对于 Java EE

I made a common Java SE abstract class to do the job.

我制作了一个通用的 Java SE 抽象类来完成这项工作。

You may copy & paste this:

您可以复制并粘贴此内容:

// 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, String key)
      throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, 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(), new byte[]{0,1,2,3,4,5,6,7}, 65536, 128);
    SecretKey tmp = factory.generateSecret(spec);
    SecretKey secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
    cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
    byte[] plainText= cipher.doFinal(cipherText);
    String plain = new String(plainText, StandardCharsets.UTF_8);
    return plain;
  }
}

Then you can use it this way:

然后你可以这样使用它:

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");

}

In case you want to use encoded properties you may enclose it's value inside ENC() and a password for decryption will be searched for in the same path and name of the property file with an added .key extension. In this example it will look for the password in the application.properties.key file.

如果您想使用编码属性,您可以将它的值包含在 ENC() 中,并且将在属性文件的相同路径和名称中搜索解密密码,并添加 .key 扩展名。在本例中,它将在 application.properties.key 文件中查找密码。

application.properties ->

application.properties ->

ws.url=http://some webside
hidden.text=ENC(AAAADCzaasd9g61MI4l5sbCXrFNaQfQrgkxygNmFa3UuB9Y+YzRuBGYj+A==)

aplication.properties.key ->

aplication.properties.key ->

password aca

For the encryption of property values for the Java EE solution I consulted Patrick Favre-Bulle excellent article on Symmetric Encryption with AES in Java and Android. Then checked the Cipher, block mode and padding in this SO question about AES/GCM/NoPadding. And finally I made the AES bits be derived from a password from @erickson excellent answer in SO about AES Password Based Encryption. Regarding encryption of value properties in Spring I think they are integrated with Java Simplified Encryption

对于 Java EE 解决方案的属性值加密,我咨询了 Patrick Favre-Bulle 关于Java 和 Android 中使用 AES 对称加密的优秀文章。然后在这个关于AES/GCM/NoPadding 的问题中检查了密码、块模式和填充。最后,我使 AES 位从@erickson 在 SO about AES Password Based Encryption 中的优秀答案中导出。关于 Spring 中值属性的加密,我认为它们与Java Simplified Encryption集成

Wether this qualify as a best practice or not may be out of scope. This answer shows how to have reloadable properties in Spring Boot and Java EE.

这是否符合最佳实践可能超出了范围。此答案显示了如何在 Spring Boot 和 Java EE 中拥有可重新加载的属性。

回答by Boris

This functionality can be achieved by using a Spring Cloud Config Serverand a refresh scope client.

此功能可以通过使用Spring Cloud Config Server刷新范围客户端来实现

Server

服务器

Server (Spring Boot app) serves the configuration stored, for example, in a Git repository:

服务器(Spring Boot 应用程序)提供存储在例如 Git 存储库中的配置:

@SpringBootApplication
@EnableConfigServer
public class ConfigServer {
  public static void main(String[] args) {
    SpringApplication.run(ConfigServer.class, args);
  }
}

application.yml:

应用程序.yml:

spring:
  cloud:
    config:
      server:
        git:
          uri: git-repository-url-which-stores-configuration.git

configuration file configuration-client.properties(in a Git repository):

配置文件configuration-client.properties(在 Git 存储库中):

configuration.value=Old

Client

客户

Client (Spring Boot app) reads configuration from the configuration server by using @RefreshScopeannotation:

客户端(Spring Boot 应用程序)使用@RefreshScope注解从配置服务器读取配置:

@Component
@RefreshScope
public class Foo {

    @Value("${configuration.value}")
    private String value;

    ....
}

bootstrap.yml:

引导程序.yml:

spring:
  application:
    name: configuration-client
  cloud:
    config:
      uri: configuration-server-url

When there is a configuration change in the Git repository:

当 Git 存储库中的配置发生更改时:

configuration.value=New

reload the configuration variable by sending a POSTrequest to the /refreshendpoint:

通过向端点发送POST请求来重新加载配置变量/refresh

$ curl -X POST http://client-url/actuator/refresh

Now you have the new value New.

现在您有了新值New

Additionally Fooclass can serve the value to the rest of application via RESTful APIif its changed to RestControllerand has a corresponding endpont.

此外FooRESTful API如果类更改为RestController并具有相应的端点,则类可以将值提供给应用程序的其余部分。

回答by Aliaksandr Kavalenka

I used @David Hofmann concept and made some changes because of not all was good. First of all, in my case I no need auto-reload, I just call the REST controller for updating properties. The second case @David Hofmann's approach not workable for me with outside files.

我使用了@David Hofmann 的概念并进行了一些更改,因为并非一切都很好。首先,在我的情况下,我不需要自动重新加载,我只需调用 REST 控制器来更新属性。第二种情况@David Hofmann 的方法对我来说不适用于外部文件。

Now, this code can work with application.propertiesfile from resources(inside the app) and from an outside place. The outside file I put near jar, and I use this --spring.config.location=app.propertiesargument when the application starts.

现在,此代码可以与来自资源(应用程序内部)和外部位置的application.properties文件一起使用。我放在 jar 附近的外部文件,我在应用程序启动时使用这个 --spring.config.location=app.properties参数。

@Component
public class PropertyReloader { 
private final Logger logger = LoggerFactory.getLogger(getClass());

@Autowired
private StandardEnvironment environment;
private long lastModTime = 0L;
private PropertySource<?> appConfigPropertySource = null;
private Path configPath;
private static final String PROPERTY_NAME = "app.properties";

@PostConstruct
private void createContext() {
    MutablePropertySources propertySources = environment.getPropertySources();
    // first of all we check if application started with external file
    String property = "applicationConfig: [file:" + PROPERTY_NAME + "]";
    PropertySource<?> appConfigPsOp = propertySources.get(property);
    configPath = Paths.get(PROPERTY_NAME).toAbsolutePath();
    if (appConfigPsOp == null) {
       // if not we check properties file from resources folder
        property = "class path resource [" + PROPERTY_NAME + "]";
        configPath = Paths.get("src/main/resources/" + PROPERTY_NAME).toAbsolutePath();
    }
    appConfigPsOp = propertySources.get(property);
    appConfigPropertySource = appConfigPsOp;
 }
// this method I call into REST cintroller for reloading all properties after change 
//  app.properties file
public void reload() {
    try {
        long currentModTs = Files.getLastModifiedTime(configPath).toMillis();
        if (currentModTs > lastModTime) {
            lastModTime = currentModTs;
            Properties properties = new Properties();
            @Cleanup InputStream inputStream = Files.newInputStream(configPath);
            properties.load(inputStream);
            String property = appConfigPropertySource.getName();
            PropertiesPropertySource updatedProperty = new PropertiesPropertySource(property, properties);
            environment.getPropertySources().replace(property, updatedProperty);
            logger.info("Configs {} were reloaded", property);
        }
    } catch (Exception e) {
        logger.error("Can't reload config file " + e);
    }
}

}

}

I hope that my approach will help somebody

我希望我的方法能帮助某人