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

szqfcxe2  于 2023-06-21  发布在  Spring
关注(0)|答案(6)|浏览(121)

许多内部解决方案浮现在脑海中。就像在数据库中的属性,并轮询它每N秒。然后检查.properties文件的时间戳修改并重新加载它。
但是我在Java EE标准和Sping Boot 文档中寻找,似乎找不到最好的方法。
我需要我的应用程序读取属性文件(或env。变量或DB参数),然后能够重新读取它们。生产中使用的最佳实践是什么?
一个正确的答案将至少解决一个场景(Sping Boot 或Java EE),并提供如何使其在另一个场景中工作的概念性线索

yc0p9oo0

yc0p9oo01#

经过进一步研究,reloading properties must be carefully considered.例如,在Spring中,我们可以重新加载属性的“当前”值,而不会遇到太多问题。但是,当在上下文初始化时基于www.example.com文件中存在的值application.properties(例如,数据源、连接池、队列等)初始化资源时,必须特别注意。

备注

用于Spring和Java EE的抽象类并不是干净代码的最佳示例。但是它很容易使用,并且它确实解决了这些基本的初始要求:

  • 不使用Java 8类以外的外部库。
  • 只有一个文件可以解决这个问题(Java EE版本大约160行)。
  • 文件系统中可用的标准Java属性UTF-8编码文件的用法。
  • 支持加密属性。
    **对于Sping Boot **

此代码有助于热重新加载application.properties文件,而无需使用Spring Cloud Config服务器(对于某些用例可能会过度)
这个抽象类你可以直接复制粘贴(好东西: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使用抽象类的www.example.com检索属性值

@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==)

application.properties.key ->

password aca

关于Java EE解决方案的属性值加密,我查阅了帕特里克Favre-Bulle关于Symmetric Encryption with AES in Java and Android的优秀文章。然后检查了关于AES/GCM/NoPadding的SO问题中的密码,块模式和填充。最后,我从@erickson的密码中导出了AES位,这是关于AES Password Based Encryption的SO中的优秀答案。关于Spring中值属性的加密,我认为它们与Java Simplified Encryption集成在一起
这是否符合最佳实践的条件可能超出了范围。这个答案展示了如何在Sping Boot 和Java EE中拥有可重新加载的属性。

ozxc1zmp

ozxc1zmp2#

这个功能可以通过使用Spring Cloud Config Server和refresh scope client来实现。

服务器

服务器(Sping 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

客户端

客户端(Sping Boot 应用程序)通过使用@RefreshScope annotation从配置服务器读取配置:

@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

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

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

现在你有了新的值New
此外,如果Foo类更改为RestController并具有相应的endpont,则Foo类可以通过RESTful API将值提供给应用程序的其余部分。

sycxhyv7

sycxhyv73#

我使用了@大卫Hofmann的概念,并做了一些修改,因为并不是所有的都是好的。首先,在我的例子中,我不需要自动重载,我只需要调用REST控制器来更新属性。第二个案例@大卫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);
    }
}

}
我希望我的方法能帮助到一些人

b5lpy0ml

b5lpy0ml4#

正如@Boris所提到的,Spring Cloud Config是避免补丁解决方案的方法。为了保持最小的设置,我建议使用本机类型(文件类型)嵌入配置服务器方法。
为了支持自动配置刷新而不需要手动调用执行器端点,我创建了一个目录侦听器来检测文件更改并分派刷新范围事件。
概念验证存储库(git

v09wglhw

v09wglhw5#

对于spring Boot ,有一篇关于here主题的非常好的文章,但是对于多个属性文件,它并不完美。在我的情况下,我有2个属性文件,一个不敏感,一个包含密码。我接着说:

<dependency>
    <groupId>commons-configuration</groupId>
    <artifactId>commons-configuration</artifactId>
    <version>1.10</version>
</dependency>

扩展spring的PropertySource,以便可以将可重载版本添加到环境中。

public class ReloadablePropertySource extends PropertySource {

    private final PropertiesConfiguration propertiesConfiguration;

    public ReloadablePropertySource(String name, String path, ConfigurationListener listener) {
        super(StringUtils.hasText(name) ? name : path);
        try {
            this.propertiesConfiguration = getConfiguration(path, listener);
        } catch (Exception e) {
            throw new MissingRequiredPropertiesException();
        }
    }

    @Override
    public Object getProperty(String s) {
        return propertiesConfiguration.getProperty(s);
    }

    private PropertiesConfiguration getConfiguration(String path, ConfigurationListener listener) throws ConfigurationException {
        PropertiesConfiguration configuration = new PropertiesConfiguration(path);
        FileChangedReloadingStrategy reloadingStrategy = new FileChangedReloadingStrategy();
        reloadingStrategy.setRefreshDelay(5000);
        configuration.setReloadingStrategy(reloadingStrategy);
        configuration.addConfigurationListener(listener);
        return configuration;
    }
}

现在将所有的属性文件(现在可重载)添加到Spring的env中

@Configuration
public class ReloadablePropertySourceConfig {

    private final ConfigurableEnvironment env;

    @Value("${spring.config.location}")
    private String appConfigPath;

    @Value("${spring.config.additional-location}")
    private String vaultConfigPath;

    public ReloadablePropertySourceConfig(ConfigurableEnvironment env) {
        this.env = env;
    }

    @Bean
    @ConditionalOnProperty(name = "spring.config.location")
    public ReloadablePropertySource getAppConfigReloadablePropertySource(){
        ReloadablePropertySource rps = new ReloadablePropertySource("dynamicNonSensitive", appConfigPath, new PropertiesChangeListener());
        MutablePropertySources sources = env.getPropertySources();
        sources.addFirst(rps);
        return rps;
    }

    @Bean
    @ConditionalOnProperty(name = "spring.config.additional-location")
    public ReloadablePropertySource getVaultReloadablePropertySource(){
        ReloadablePropertySource rps = new ReloadablePropertySource("dynamicVault", vaultConfigPath, new PropertiesChangeListener());
        MutablePropertySources sources = env.getPropertySources();
        sources.addFirst(rps);
        return rps;
    }

    private static class PropertiesChangeListener implements ConfigurationListener{

        @Override
        public void configurationChanged(ConfigurationEvent event) {
            if (!event.isBeforeUpdate()){
                System.out.println("config refreshed!");
            }
        }
    }
}

article
我们已经添加了新的属性源作为第一项,因为我们希望它用相同的键覆盖任何现有的属性
在我们的例子中,我们有2个“可重载”的属性源,这两个都将首先被查找。
最后再创建一个类,我们可以从这个类访问env的属性

@Component
public class ConfigProperties {

    private final Environment environment;

    public ConfigProperties(Environment environment) {
        this.environment = environment;
    }

    public String getProperty(String name){
        return environment.getProperty(name);
    }
}

现在,您可以自动连接ConfigProperties并始终获取文件中的最新属性,而无需重新启动应用程序。

@RestController
@Slf4j
public class TestController {

    @Autowired
    private ConfigProperties env;

    @GetMapping("/refresh")
    public String test2() {
        log.info("hit");
        String updatedProperty = env.getProperty("test.property");
        String password = env.getProperty("db.password");
        return updatedProperty + "\n" + password;
    }

}

其中test.property来自第一个文件,db.password来自另一个文件。

wmomyfyw

wmomyfyw6#

如果您想实时更改属性,并且不想重新启动服务器,请按照以下步骤操作:
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).将www.example.com放在application.properties/target/config文件夹中。在/target文件夹中创建jar
4).在www.example.com下面添加一个类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).运行spring Boot 应用程序
从浏览器调用localhost:XXXX/test
Output : xyz
7).将www.example.com中的值application.properties从xyz更改为abc
8).使用postman发送POST请求到localhost:XXXX/actuator/refresh
response: ["app.name"]
9).从浏览器调用localhost:XXXX/find
Output : abc

相关问题