Как выполнить горячую перезагрузку свойств в Java EE и Spring Boot?

На ум приходят многие собственные решения. Например, иметь свойства в базе данных и опрашивать их каждые N секунд. Затем также проверьте изменение метки времени для файла .properties и перезагрузите его.

Но я искал стандарты Java EE и загрузочные документы Spring, и, похоже, не нашел лучшего способа сделать это.

Мне нужно, чтобы мое приложение считывало файл свойств (или переменные окружения или параметры БД), а затем могло их перечитать. Какая передовая практика используется в производстве?

Правильный ответ, по крайней мере, решит один сценарий (Spring Boot или Java EE) и даст концептуальный ключ к тому, как заставить его работать в другом.

Взгляните на Spring Cloud Config.

chrylis -cautiouslyoptimistic- 01.10.2018 18:11

Лично я просто добавил / предоставил веб-сервис, который можно было бы назвать инструктирующим приложение перезагрузить его свойства - так что, хотя это, возможно, не лучшая практика, у меня это сработало. Это также минимизирует потребление ресурсов, поскольку не выполняет никаких непрерывных опросов.

JGlass 01.10.2018 18:12

Это то, что я имел в виду. Я просто упростил вопрос. Спасибо!

David Hofmann 01.10.2018 18:13

Возможный дубликат: stackoverflow.com/questions/26150527/…

Eliseo Ocampos 01.10.2018 20:37

Этот вопрос касается как весенней загрузки, так и Java ee полуконцептуальным образом.

David Hofmann 02.10.2018 02:15

@BalusC, вы отредактировали вопрос, как будто это проблема только с весенней загрузкой, указав, что это не проблема java SE. Я написал Java EE, просьба не редактировать вопрос. Это действительно так.

David Hofmann 03.10.2018 00:16

Пользователь Java SE не может ответить на ваш вопрос. Spring Boot не является частью Java EE, это часть Spring.

BalusC 03.10.2018 12:44

Я склонен думать, что это больше связано с JavaSE, чем с SpringBoot и / или JavaEE, поскольку вероятным решением будет простой вспомогательный класс JavaSE.

David Hofmann 03.10.2018 16:07
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
24
8
47 695
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Эта функциональность может быть достигнута с помощью Сервер Spring Cloud Config и клиент обновленной области.

Сервер

Сервер (приложение Spring Boot) обслуживает конфигурацию, хранящуюся, например, в репозитории 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

Клиент

Клиент (приложение Spring Boot) считывает конфигурацию с сервера конфигурации с помощью аннотации @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 может передавать значение остальной части приложения через RESTful API, если он изменен на RestController и имеет соответствующий конец.

Что, если мы не хотим хранить конфигурацию в облаке?

user666 27.12.2018 13:39

@ user666 Где вы хотите его хранить? Каковы ваши требования?

Boris 27.12.2018 14:58

Я хочу просто перезагружать файл application.properties каждый раз при его изменении без перезапуска приложения. Пример изменения IP-адреса базы данных или любой другой конфигурации. Файл хранится на сервере рядом с Java Jar.

user666 28.12.2018 08:00

Например, если мы хотим изменить пароль исполнительного механизма ... Кстати, я пытался протестировать конечную точку обновления, я использую зависимости исполнительного механизма и облака Spring, когда я вызываю конечную точку обновления из браузера, я получаю: {"timestamp «:« 2018-12-28T07: 01: 17.767 + 0000 »,« status »: 405,« er‌ ror »:« Метод не разрешен »,« message »:« Метод запроса 'GET' не поддерживается »,« путь ":" / активатор / обновление "}

user666 28.12.2018 08:01

Я попытался использовать curl и опубликовать его, чтобы получить запрещенный доступ, возможно, потому, что я использую безопасность Spring с именем пользователя и паролем, как я могу этого добиться?

user666 28.12.2018 08:07

Я попытался использовать curl и опубликовать, что он дает запрещенный доступ: $ curl -X POST -u user: test локальный: 8081 / исполнительный механизм / обновление% Total% Received% Xferd Средняя скорость Время Время Время Текущая загрузка загрузки Всего затрачено Оставшаяся скорость 100 126 0 126 0 0 448 0 - : -: - -: -: - -: -: - 448 {"timestamp": "2018-12-28T07: 12: 09.083 + 0000", "status": 403, ‌ "error": "Forbidden", ‌ "message": "Forbidden‌", "path": "/ actator / ‌ refresh"}

user666 28.12.2018 08:12

Я исправил проблему с обновлением с помощью stackoverflow.com/questions/52299072/…

user666 28.12.2018 08:19

Мне удалось перезагрузить конфигурацию с помощью конечной точки обновления, однако она не перезагружает, например, uri источника данных, а просто перезагружает другую конфигурацию, вызываемую в коде программно. Любые советы о том, как перезагрузить информацию об источнике данных?

user666 28.12.2018 08:36
Ответ принят как подходящий

После дальнейшего исследования перезаряжаемые свойства должны быть тщательно рассмотрены. В Spring, например, мы можем без особых проблем перезагрузить «текущие» значения свойств. Но. Следует проявлять особую осторожность при инициализации ресурсов во время инициализации контекста на основе значений, которые присутствовали в файле application.properties (например, источники данных, пулы соединений, очереди и т. д.).

ПРИМЕЧАНИЕ:

Абстрактные классы, используемые для Spring и Java EE, не являются лучшим примером чистого кода. Но его легко использовать, и он отвечает этим основным начальным требованиям:

  • Запрещается использование внешних библиотек, кроме классов Java 8.
  • Только один файл для решения проблемы (~ 160 строк для версии Java EE).
  • Использование стандартного файла свойств Java в кодировке UTF-8, доступного в файловой системе.
  • Поддержка зашифрованных свойств.

Для весенней загрузки

Этот код помогает с горячей перезагрузкой файла application.properties без использования сервера Spring Cloud Config (что может быть излишним для некоторых случаев использования)

Этот абстрактный класс вы можете просто скопировать и вставить (SO вкусности: 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();
}

Затем вы создаете класс компонента, который позволяет извлекать значения свойств из applicationatoin.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);
   }
}

Теперь вы можете автоматическая проводка Bean AppProperties везде, где вам это нужно. Просто убедитесь, что всегда вызывает в нем методы, а не сохраняет его значение в переменной. И не забудьте перенастроить любой ресурс или 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 я обратился к отличной статье Патрика Фавра-Булля о Симметричное шифрование с AES в Java и Android. Затем проверил шифр, режим блока и заполнение в этом вопросе SO о AES / GCM / NoPadding. И, наконец, я сделал биты AES производными от пароля от @erickson, отличный ответ в SO о Шифрование на основе пароля AES. Что касается шифрования свойств значений в Spring, я думаю, что они интегрированы с Упрощенное шифрование Java

Можно ли это квалифицировать как лучшую практику или нет. Этот ответ показывает, как иметь перезагружаемые свойства в Spring Boot и Java EE.

Я использовал концепцию @David Hofmann и внес некоторые изменения, потому что не все было хорошо. Прежде всего, в моем случае мне не нужна автоперезагрузка, я просто вызываю REST-контроллер для обновления свойств. Во втором случае подход @David Hofmann не работает для меня с внешними файлами.

Теперь этот код может работать с файлом 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);
    }
}

}

Надеюсь, мой подход кому-то поможет

Как упоминал @Boris, Spring Cloud Config - это способ избежать неоднородного решения. Чтобы сохранить минимальные настройки, я предлагаю Внедрение подхода сервера конфигурации с собственным типом (типом файла).

Чтобы поддерживать автоматическое обновление конфигурации без ручного вызова конечной точки исполнительного механизма, я создал прослушиватель каталога для обнаружения изменений файлов и отправки события области действия обновления.

Репо Proof Of Concept (мерзавец)

Если вы хотите изменить свойства в реальном времени и не хотите перезапускать сервер, выполните следующие действия:

1). Application.properties

app.name= xyz
management.endpoints.web.exposure.include=*

2). Добавьте ниже зависимости в pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-context</artifactId>
        <version>2.1.1.RELEASE</version>
    </dependency>

3) Поместите application.properties в папку /target/config. Создайте банку в папке /target

4). Добавьте классы ниже ApplcationProperties.java

@Component
@RefreshScope
@ConfigurationProperties(prefix = "app")
public class ApplicationProperties {
private String name;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}
}

5). Напишите Controller.java и вставьте ApplcationProperties

@RestController
public class TestController {

@Autowired
private ApplicationProperties applcationProperties;

@GetMapping("/test")
public String getString() {
    return applcationProperties.getName();
}
}

6) .Запустите приложение весенней загрузки.

Позвоните localhost:XXXX/test из вашего браузера

Output : xyz

7). Измените значение в application.properties с xyz на abc

8). Используя почтальон, отправьте запрос POST на localhost: XXXX / actator / refresh

response: ["app.name"]

9). Вызовите localhost: XXXX / найти из своего браузера

Output : abc

Другие вопросы по теме