当前位置: 首页 > 面试题库 >

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

孔光赫
2023-03-14
问题内容

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

但是我一直在寻找Java EE标准和Spring Boot文档,但似乎找不到最佳的方法。

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

正确的答案至少可以解决一种情况(Spring Boot或Java EE),并提供有关如何使其在另一种情况下工作的概念性提示


问题答案:

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

但是我一直在寻找Java EE标准和Spring Boot文档,但似乎找不到最佳的方法。

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

正确的答案至少可以解决一种情况(Spring Boot或Java EE),并提供有关如何使其在另一种情况下工作的概念性提示用于Spring和Java EE的抽象类不是干净代码的最佳示例。但是它易于使用,并且确实满足了以下基本初始要求:

  • 除Java 8类外,不使用任何外部库。
  • 仅一个文件即可解决问题(Java EE版本约为160行)。
  • 文件系统中提供的标准Java属性UTF-8编码文件的使用。
  • 支持加密的属性。

For Spring Boot

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

你可以只复制并粘贴此抽象类(SO goodies:D),这是从该SO答案派生的代码

// 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, 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;
  }
}

然后,你可以通过以下方式使用它:

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解决方案的属性值的加密,我查阅了Patrick Favre-Bulle的出色文章,内容涉及Java和Android中的AES对称加密。然后在关于AES / GCM / NoPadding的 SO问题中检查了密码,块模式和填充。最后,我使AES位元从@erickson的密码中获得了一个很好的答案,有关SO 密码的加密。关于Spring中的值属性加密,我认为它们与Java简化加密集成在一起

是否可以将此视为最佳做法,否则可能不在范围之内。该答案显示了如何在Spring Boot和Java EE中具有可重载的属性。



 类似资料:
  • 但是我查看了Java EE标准和Spring Boot文档,似乎找不到最好的方法。 我需要我的应用程序读取属性文件(或env.variables或DB参数),然后能够重新读取它们。生产中使用的最佳实践是什么? 一个正确的答案将至少解决一个场景(Spring Boot或Java EE),并提供如何使其在另一个场景中工作的概念线索

  • 本文向大家介绍Springboot教程之如何设置springboot热重启,包括了Springboot教程之如何设置springboot热重启的使用技巧和注意事项,需要的朋友参考一下 SpringBoot热重启步骤 1.打开点击pom.xml配置文件 2.找到配置文件节点 3.在节点中插入以下代码 4.点击编辑器菜单栏view ->Tool Windows->Maven Projects 中查看是

  • "热重载"不是当你修改文件的时候简单重新加载页面。启用热重载后,当你修改 .vue 文件时,所有该组件的实例会被替换,并且不需要刷新页面。它甚至保持应用程序和被替换组件的当前状态!当你调整模版或者修改样式时,这极大的提高了开发体验。 状态保留规则 当编辑一个组件的 <template> 时,这个组件实例将就地重新渲染,并保留当前所有的私有状态。能够做到这一点是因为模板被编译成了新的无副作用的渲染函

  • Vuex 支持在开发中使用webpack的Hot Module Replacement API 热重载 mutations,modules ,actions 和 getters,你也可以在 Browserify 里使用 browserify-hmr 插件来实现同样的功能。 对于mutations 和 modules,你只需要简单地调用 store.hotUpdate(): // store.js

  • 我正在用Springboot和Angular 8创建一个应用程序。我对Springboot是新手。我的计划是在中构建前端(-将从)调用。 另一方面,如果我使用启动前端(在开发过程中),那么由于跨源问题,它将无法工作。

  • 问题内容: 我很好奇,是否有人知道Grails或Play这样的框架如何检测代码更改并自动触发重新编译而无需重新启动应用程序服务器的详细信息?Groovy的编译器是否具有某些特定功能,或者它的动态特性使这种操作很容易实现? 对于背景,我在构建过程中有一个自定义代码生成阶段,我希望能够具有类似的编辑和刷新功能。 在此先感谢您提供任何指针,即使我必须仔细检查代码才能获得更大的图像。 编辑: 我应该澄清一