web-dev-qa-db-ja.com

Java EEおよびSpring Bootでプロパティをホットリロードする方法は?

多くの社内ソリューションが思い浮かびます。データベースにプロパティを持ち、N秒ごとにポーリングするのが好きです。次に、.propertiesファイルのタイムスタンプの変更も確認し、再読み込みします。

しかし、私はJava EE標準とスプリングブートドキュメントを探していましたが、それを行うための最良の方法を見つけることができないようです。

アプリケーションでプロパティファイル(またはenv。変数またはDBパラメーター)を読み取り、それらを再読み取りできるようにする必要があります。本番環境で使用されているベストプラクティスは何ですか?

正解は、少なくとも1つのシナリオ(Spring BootまたはJava EE)を解決し、他のシナリオで動作させる方法についての概念的な手がかりを提供します。

9
David Hofmann

さらなる調査の後、 リロードプロパティを慎重に検討する必要があります 。たとえば、Springでは、プロパティの「現在の」値を問題なく再読み込みできます。しかし。 application.propertiesファイルに存在する値(データソース、接続プール、キューなど)に基づいてコンテキストの初期化時にリソースが初期化された場合、特別な注意が必要です。

[〜#〜] note [〜#〜]

SpringおよびJava EEに使用される抽象クラスは、クリーンなコードの最良の例ではありません。しかし、使いやすく、この基本的な初期要件に対処します。

  • Java 8クラス。
  • 問題を解決するためのファイルは1つだけです(Java EEバージョン)の場合、〜160行)。
  • 標準の使用法Javaファイルシステムで利用可能なプロパティUTF-8エンコードファイル。
  • 暗号化されたプロパティをサポートします。

Spring Bootの場合

このコードは、Spring Cloud Configサーバーを使用せずにapplication.propertiesファイルをホットリロードするのに役立ちます(一部のユースケースでは過剰になる可能性があります)

この抽象クラスは、コピーして貼り付けるだけです(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();
}

次に、抽象クラスを使用するapplicatoin.propertiesからプロパティ値を取得できるBeanクラスを作成します

@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をauto-wireできます。値を変数に保存する代わりに、必ずalwaysでメソッドを呼び出すようにしてください。そして、潜在的に異なるプロパティ値で初期化されたリソースまたはBeanを必ず再構成してください。

今のところ、私はこれを外部およびデフォルトで見つかった./config/application.propertiesファイルでのみテストしました。

For 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の素晴らしい記事 AESを使用した対称暗号化Java and Android 。次に、Cipher、ブロックモード、およびパディングをチェックしましたSO AES/GCM/NoPadding に関する質問。そして最後にAESビットを派生させましたSO about AES Password Based Encryption の@ericksonの優れた回答からのパスワード。Springの値プロパティの暗号化に関しては、これらは Java Simplified暗号化

これがベストプラクティスとして適格であるかどうかは、範囲外である可能性があります。この答えは、Spring BootおよびJava EE。

8
David Hofmann

この機能は、 Spring Cloud Config Serverrefresh scope client を使用して実現できます。

サーバー

サーバー(スプリングブートアプリ)は、たとえばGitリポジトリに保存されている構成を提供します。

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

application.yml:

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

構成ファイルconfiguration-client.properties(Gitリポジトリ内):

configuration.value=Old

クライアント

クライアント(スプリングブートアプリ)は、 @ RefreshScope アノテーションを使用して、構成サーバーから構成を読み取ります。

@Component
@RefreshScope
public class Foo {

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

    ....
}

bootstrap.yml:

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

Gitリポジトリーの構成が変更された場合:

configuration.value=New

POSTリクエストを/refreshエンドポイントに送信して、構成変数を再読み込みします。

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

これで、新しい値Newが得られました。

さらに、Fooクラスは、RestControllerに変更され、対応するendpontがある場合、RESTful APIを介してアプリケーションの残りの部分に値を提供できます。

5
Boris